/* ==========================================================================
   EXOFEAST — storefront
   Ported from exofeast-homepage.html. The :root block below is the token
   source of truth named in the brand board (§09.1); the backoffice mirrors
   it in admin/css/app.css. Change a colour here and there, or in neither.

   White theme is locked. Do not reintroduce the dark direction.
   ========================================================================== */

:root{
  --background:#FFFFFF;
  --foreground:#161B10;
  --muted:#F2F5EE;
  --hint:#6B7860;--placeholder:#8A9680;--muted-foreground:#5F6B55;
  --secondary:#F6F8F2;
  --border:#E1E7D9;
  --primary:#65A30D;            /* lime, darkened for white-bg contrast */
  --primary-foreground:#FFFFFF;
  --neon:#A3E635;               /* glows & dark surfaces only, never text on white */
  --frost:#5E8A90;              /* FROZEN badge convention */
  --alert:#C4563E;              /* low stock / destructive only */
  --font-display:'Oswald',sans-serif;
  --font-body:'Inter',sans-serif;
  --font-mono:'Spline Sans Mono',monospace;
  --radius:8px;

  /* Elevation. Three surfaces rather than one flat white, so a card reads as
     sitting ON the page instead of being a rectangle drawn on it. */
  --surface-0:#FFFFFF;
  --surface-1:#FCFDFA;
  --surface-2:#F6F8F2;
  /* Shadows tinted with the brand's ink rather than neutral black — a grey
     shadow on a warm-white ground is the commonest thing that makes a site
     look cheap. */
  --shadow-sm:0 1px 2px rgba(22,27,16,.04), 0 1px 3px rgba(22,27,16,.03);
  --shadow-md:0 2px 4px rgba(22,27,16,.04), 0 8px 24px rgba(22,27,16,.06);
  --shadow-lg:0 4px 8px rgba(22,27,16,.05), 0 18px 48px rgba(22,27,16,.10);
  --ease:cubic-bezier(.22,.61,.36,1);
}

*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{background:var(--background);color:var(--foreground);font-family:var(--font-body);line-height:1.5;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility}
/* Prices and counts line up column to column. Proportional digits in a price
   list is a small thing that reads as unconsidered. */
.prod-price,.pdp-price,.line__price,.totals,.summary-line,.pay-amount,.ladder__price,
.order-row__total,.receipt__no,.mono{font-variant-numeric:tabular-nums}
img{display:block}
a{text-decoration:none;color:inherit}
button{font-family:inherit;cursor:pointer;background:none;border:none;color:inherit}
.container{max-width:1280px;margin:0 auto;padding:0 24px;width:100%}
.font-display{font-family:var(--font-display)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* The [hidden] attribute must beat every display rule in this file — any author
 * rule setting `display` outbeats the browser's `[hidden]{display:none}`, and
 * the failure is silent (the element renders while the DOM reports it hidden).
 * The backoffice shipped with exactly that bug on its auth gate. */
[hidden]{display:none!important}

/* neon effects — the glow recipe is fixed by the brand board (§02) */
.neon-text{text-shadow:0 0 14px rgba(132,204,22,.35),0 0 36px rgba(132,204,22,.18)}
.neon-glow{box-shadow:0 0 16px rgba(101,163,13,.35)}
.gradient-card{background:linear-gradient(145deg,#FDFEFB 0%,#F2F6EC 100%)}

/* ---------- header ---------- */
header.site{position:sticky;top:0;z-index:50;border-bottom:1px solid var(--border);background:rgba(255,255,255,.85);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}
.header-inner{display:flex;height:64px;align-items:center;justify-content:space-between}
.logo-wrap{display:flex;align-items:center;gap:8px}
/* min-height matches .logo-img so the header does not change height when the
   wordmark is replaced by an uploaded logo. On a first visit, before the
   settings cache exists, that swap still happens — it just no longer moves the
   nav underneath it. */
.logo-mark{display:inline-flex;align-items:center;min-height:40px}
.logo-img{height:40px;width:auto;max-width:200px;object-fit:contain}
.logo-fallback{font-family:var(--font-display);font-weight:700;font-size:22px;letter-spacing:.06em;text-transform:uppercase}
.logo-fallback span{color:var(--primary)}
nav.desktop{display:none;align-items:center;gap:32px}
nav.desktop a{font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:.18em;color:var(--muted-foreground);transition:color .2s}
nav.desktop a:hover,nav.desktop a.active{color:var(--primary)}
.header-actions{display:flex;align-items:center;gap:16px}
.icon-btn{color:var(--foreground);transition:color .2s;position:relative;display:inline-flex}
.icon-btn:hover{color:var(--primary)}
.icon-btn svg{width:20px;height:20px}
.menu-btn{display:inline-flex}
.menu-btn svg{width:24px;height:24px}
.cart-count{position:absolute;top:-7px;right:-9px;background:var(--primary);color:var(--primary-foreground);font-size:10px;font-weight:700;min-width:16px;height:16px;border-radius:999px;display:none;align-items:center;justify-content:center;padding:0 4px}
.cart-count.show{display:flex}
#mobileNav{display:none;flex-direction:column;gap:4px;padding:12px 24px 20px;border-bottom:1px solid var(--border);background:rgba(255,255,255,.98)}
#mobileNav.open{display:flex}
#mobileNav a{padding:10px 0;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:.18em;color:var(--muted-foreground)}
#mobileNav a.active{color:var(--primary)}
@media(min-width:768px){
  .header-inner{height:80px}
  nav.desktop{display:flex}
  .menu-btn{display:none}
  .logo-img{height:48px;max-width:260px}
  .logo-mark{min-height:48px}
}

/* announcement strip — free-shipping threshold, read from settings/store */
.announce{background:var(--foreground);color:#EDF2E6;font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-align:center;padding:8px 24px}
.announce b{color:var(--neon);font-weight:500}
.announce[hidden]{display:none}

/* ---------- hero ----------
   The photograph is a snake on a white ground with the subject on the RIGHT and
   clean white on the LEFT — so the layout puts the headline in that empty space
   rather than fighting the image for it. Everything below serves that: the
   image is anchored right, and the white gradients only have to soften the seam
   because the photo's own background is already the page colour.

   Softer stops than the original draft used: the earlier overlay was written
   for a dark hero and washed a light photograph out to nearly nothing. */
.hero{position:relative;display:flex;min-height:78vh;align-items:center;overflow:hidden;
  background:var(--background)}
.hero-bg{position:absolute;inset:0}
.hero-bg .ph,.hero-bg img{height:100%;width:100%;object-fit:cover;object-position:82% center}
/* A slow drift inward. Fifteen seconds is long enough to read as depth rather
   than movement — a hero that visibly animates looks like a slideshow. */
@keyframes heroDrift{from{transform:scale(1.06)}to{transform:scale(1)}}
.hero-bg img{animation:heroDrift 15s var(--ease) both}
.hero-bg .ph{background:
  radial-gradient(ellipse 60% 50% at 75% 40%,rgba(132,204,22,.18),transparent 60%),
  linear-gradient(135deg,#F3F7EC 0%,#FAFCF6 45%,#FFFFFF 100%)}

/* Left-to-right white, holding the text column solid and releasing the subject.
   Stops chosen against this photograph: opaque to 30%, gone by 72%. */
.hero-grad-r{position:absolute;inset:0;background:linear-gradient(to right,
  var(--background) 0%, var(--background) 30%,
  rgba(255,255,255,.82) 52%, rgba(255,255,255,.18) 72%, rgba(255,255,255,0) 100%)}
/* Bottom fade into the trust bar, so the section ends rather than being cut. */
.hero-grad-t{position:absolute;inset:0;background:linear-gradient(to top,
  var(--background) 0%, rgba(255,255,255,.55) 12%, transparent 34%)}
/* A single lime bloom behind the subject — the brand's glow, used once and
   large, rather than sprinkled. */
.hero-glow{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(ellipse 46% 52% at 74% 44%,rgba(132,204,22,.16),transparent 62%)}

.hero-content{position:relative;z-index:10}
.hero-inner{max-width:620px;padding:96px 0}
.eyebrow{font-family:var(--font-display);font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.3em;color:var(--primary)}
h1{margin-top:16px;font-family:var(--font-display);font-weight:700;font-size:40px;line-height:1.06;text-transform:uppercase;letter-spacing:-.01em}
.hero-sub{margin-top:22px;font-size:18px;color:var(--muted-foreground);max-width:44ch}
.hero-ctas{margin-top:34px;display:flex;flex-wrap:wrap;gap:16px}
/* The hero copy rises in once, slightly after the image, so the section
   assembles in a deliberate order instead of everything at once. */
.hero-inner > *{animation:rise .7s var(--ease) both}
.hero-inner > *:nth-child(2){animation-delay:.08s}
.hero-inner > *:nth-child(3){animation-delay:.16s}
.hero-inner > *:nth-child(4){animation-delay:.24s}

@media(min-width:768px){.hero{min-height:86vh}h1{font-size:60px}.hero-sub{font-size:20px}}
@media(min-width:1024px){h1{font-size:72px}}

/* Below 768px the text sits ON the photograph rather than beside it, so the
   left-to-right wash protects nothing. Swap to a vertical scrim and pull the
   subject into frame. */
@media(max-width:767px){
  .hero{min-height:auto}
  .hero-inner{padding:56px 0 72px}
  .hero-bg img{object-position:70% center}
  .hero-grad-r{background:linear-gradient(to bottom,
    rgba(255,255,255,.94) 0%, rgba(255,255,255,.88) 42%,
    rgba(255,255,255,.55) 72%, rgba(255,255,255,.25) 100%)}
  .hero-glow{background:radial-gradient(ellipse 80% 40% at 60% 78%,rgba(132,204,22,.16),transparent 65%)}
}

/* ---------- buttons ---------- */
.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:6px;background:var(--primary);color:var(--primary-foreground);padding:12px 24px;font-family:var(--font-display);font-size:14px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;transition:box-shadow .25s;border:1px solid var(--primary)}
.btn-primary:hover{box-shadow:0 0 16px rgba(101,163,13,.4)}
.btn-primary svg{width:16px;height:16px}
.btn-primary:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}
.btn-outline{display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:6px;border:1px solid var(--border);padding:12px 24px;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--foreground);transition:border-color .2s,color .2s;background:transparent}
.btn-outline:hover{border-color:var(--primary);color:var(--primary)}
.btn-sm{padding:8px 14px;font-size:12px}
.btn-block{width:100%}

/* ---------- badges ---------- */
.badge{display:inline-flex;align-items:center;gap:5px;font-family:var(--font-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;padding:4px 10px;border-radius:999px;white-space:nowrap}
.badge-live{background:rgba(101,163,13,.1);color:var(--primary);border:1px solid rgba(101,163,13,.35)}
.badge-frozen{background:rgba(94,138,144,.1);color:var(--frost);border:1px solid rgba(94,138,144,.35)}
.badge-low{background:rgba(196,86,62,.08);color:var(--alert);border:1px solid rgba(196,86,62,.35)}
.badge-out{background:var(--muted);color:var(--muted-foreground);border:1px solid var(--border)}

/* ---------- trust bar ----------
   Was three muted labels in a row, which read as filler — "Quality Guaranteed"
   with nothing behind it is what every shop says. Now each claim carries the
   reason it is credible, the icon sits in a lime disc so the row has rhythm,
   and hairline dividers stop the three merging into one grey line. */
.trust{border-top:1px solid var(--border);border-bottom:1px solid var(--border);
  background:linear-gradient(180deg,var(--surface-2) 0%, var(--surface-1) 100%)}
.trust-grid{display:grid;grid-template-columns:1fr;gap:0}
.trust-item{display:flex;align-items:flex-start;gap:14px;padding:22px 0;
  border-bottom:1px solid var(--border)}
.trust-item:last-child{border-bottom:none}
.trust-icon{display:inline-flex;align-items:center;justify-content:center;
  width:42px;height:42px;flex-shrink:0;border-radius:999px;
  background:rgba(101,163,13,.09);border:1px solid rgba(101,163,13,.22);
  color:var(--primary)}
.trust-icon svg{width:20px;height:20px}
.trust-text{display:flex;flex-direction:column;gap:3px;min-width:0}
.trust-text strong{font-family:var(--font-display);font-size:14px;font-weight:600;
  letter-spacing:.09em;text-transform:uppercase;color:var(--foreground)}
.trust-text em{font-style:normal;font-size:13.5px;color:var(--muted-foreground);line-height:1.5}
@media(min-width:768px){
  .trust-grid{grid-template-columns:repeat(3,1fr);gap:0;padding-top:30px;padding-bottom:30px}
  .trust-item{padding:4px 32px;border-bottom:none;border-left:1px solid var(--border)}
  .trust-item:first-child{border-left:none;padding-left:0}
  .trust-item:last-child{padding-right:0}
}

/* ---------- sections ---------- */
/* Section rhythm.
   These are HALF the gap, not the gap: two adjacent sections each contribute
   their padding, so 96px here produced 192px between one section's content and
   the next — which is what made the homepage read as a stack of separate pages
   rather than one. 40px gives 80px between sections on desktop, 56 on mobile.

   Padding rather than margin because .featured is a tinted band: the tint is
   drawn by the section box, so its breathing room has to be inside it. */
section.block{padding:28px 0}
@media(min-width:768px){section.block{padding:40px 0}}
/* The tinted band carries a little more, or the tint hugs the heading. */
section.block.featured{padding:40px 0}
@media(min-width:768px){section.block.featured{padding:56px 0}}
h2{font-family:var(--font-display);font-size:24px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
@media(min-width:768px){h2{font-size:30px}}
.section-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}
.view-all{font-size:14px;font-weight:500;color:var(--primary)}
.view-all:hover{text-decoration:underline}
.page-head{padding:48px 0 8px}
.page-head p{margin-top:12px;color:var(--muted-foreground);max-width:620px}

/* category cards */
.cat-grid{margin-top:40px;display:grid;gap:24px}
@media(min-width:768px){.cat-grid{grid-template-columns:1fr 1fr}}
.cat-card{position:relative;display:block;aspect-ratio:16/9;overflow:hidden;border-radius:var(--radius);border:1px solid var(--border);transition:border-color .25s,box-shadow .25s}
.cat-card:hover{border-color:var(--primary);box-shadow:0 0 12px rgba(101,163,13,.22)}
.cat-card img,.cat-card .ph{height:100%;width:100%;object-fit:cover;transition:transform .7s}
.cat-card:hover img,.cat-card:hover .ph{transform:scale(1.1)}
.cat-card .ph{display:flex;align-items:center;justify-content:center;color:rgba(77,124,15,.65);font-family:var(--font-display);font-size:13px;letter-spacing:.2em;text-transform:uppercase}
.cat-card .ph.rodents{background:linear-gradient(135deg,#EAF1DF,#F7FAF2)}
.cat-card .ph.insects{background:linear-gradient(135deg,#E4EED6,#F5F9EF)}
.cat-overlay{position:absolute;inset:0;background:linear-gradient(to top,rgba(255,255,255,.95) 0%,rgba(255,255,255,.45) 50%,transparent 100%)}
.cat-label{position:absolute;bottom:0;left:0;padding:24px}
.cat-label h3{font-family:var(--font-display);font-size:20px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
.cat-label p{margin-top:4px;font-size:14px;color:var(--muted-foreground)}
@media(min-width:768px){.cat-label h3{font-size:24px}}

/* ---------- product grid ---------- */
.featured{border-top:1px solid var(--border);background:rgba(246,248,242,.6)}
.prod-grid{margin-top:40px;display:grid;gap:24px}
@media(min-width:640px){.prod-grid{grid-template-columns:1fr 1fr}}
@media(min-width:1024px){.prod-grid{grid-template-columns:repeat(3,1fr)}}
.prod-card{overflow:hidden;border-radius:var(--radius);border:1px solid var(--border);transition:border-color .25s,box-shadow .25s;display:flex;flex-direction:column}
.prod-card:hover{border-color:var(--primary);box-shadow:0 0 12px rgba(101,163,13,.22)}
.prod-img{display:block;aspect-ratio:1/1;overflow:hidden;background:var(--muted);position:relative}
.prod-img img,.prod-img .ph{height:100%;width:100%;object-fit:cover;transition:transform .5s}
.prod-card:hover .prod-img img,.prod-card:hover .prod-img .ph{transform:scale(1.1)}
.prod-img .ph{display:flex;align-items:center;justify-content:center;text-align:center;padding:16px;color:rgba(77,124,15,.6);font-family:var(--font-display);font-size:12px;letter-spacing:.16em;text-transform:uppercase;background:linear-gradient(145deg,#EFF4E6,#FAFCF6)}
.prod-img .ph.frozen{background:linear-gradient(145deg,#E9F1F1,#F6FAFA);color:rgba(94,138,144,.7)}
.prod-img__badge{position:absolute;top:12px;left:12px;z-index:2}
.prod-body{padding:16px;display:flex;flex-direction:column;flex:1}
.prod-cat{font-size:12px;font-weight:500;text-transform:uppercase;letter-spacing:.18em;color:var(--primary)}
.prod-species{font-family:var(--font-mono);font-size:11.5px;font-style:italic;color:var(--muted-foreground);margin-top:6px}
.prod-name{margin-top:4px;font-weight:600;font-size:16px}
.prod-variant{margin-top:12px;width:100%}
.prod-row{margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:12px}
.prod-price{font-size:18px;font-weight:700}
.prod-foot{margin-top:auto}
.add-btn{display:inline-flex;align-items:center;gap:8px;border-radius:6px;background:var(--primary);color:var(--primary-foreground);padding:8px 12px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;transition:box-shadow .25s}
.add-btn:hover{box-shadow:0 0 16px rgba(101,163,13,.4)}
.add-btn:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}
.add-btn svg{width:14px;height:14px}

/* ---------- filters ---------- */
.filters{margin-top:28px;display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.filters__group{display:inline-flex;flex-wrap:wrap;gap:10px}
.filters__divider{width:1px;height:24px;background:var(--border);margin:0 4px}
.filters__search{max-width:260px}
.filters__sort{max-width:170px}
.chip{border:1px solid var(--border);border-radius:999px;padding:7px 16px;font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-foreground);transition:.2s}
.chip:hover{border-color:var(--primary);color:var(--primary)}
.chip.is-active{background:var(--primary);border-color:var(--primary);color:#fff}
.filters__spacer{flex:1}

/* Stacked, the row falls apart: the divider wraps to the end of a line as a
   stranded 1px tick, the flex spacer pushes nothing anywhere, and search and
   sort each claim a whole row — roughly half a phone screen of chrome before
   the first product. Chips wrap on their own line; search and sort share the
   next one. */
@media(max-width:720px){
  .filters{gap:8px}
  .filters__divider,
  .filters__spacer{display:none}
  .filters__group{flex:1 1 100%}
  .filters__search{flex:1 1 60%;max-width:none;min-width:0}
  .filters__sort{flex:1 1 34%;max-width:none;min-width:0}
}
.input{width:100%;border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--font-body);font-size:14px;color:var(--foreground);background:#fff}
.input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(101,163,13,.12)}
select.input{cursor:pointer}
label.field{display:block;margin-bottom:14px}
label.field > span{display:block;font-family:var(--font-mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground);margin-bottom:6px}
.field-row{display:grid;gap:14px}
@media(min-width:640px){.field-row{grid-template-columns:1fr 1fr}}

/* ---------- field guidance vs actual values ----------
   Reported 2026-08-23: predefined values and field guidance were hard to tell
   apart from what a person had actually typed.

   ::placeholder was never styled in either app, so it inherited the input's own
   colour and read as a real entered value — the main source of the confusion.

   Two different jobs, two different weights:
     --hint         guidance that has to stay READABLE. Kept at AA contrast
                    (>4.5:1 on white) because it carries instructions.
     --placeholder  an example of what goes here, and never the only place the
                    information lives — the <label> above it always says what
                    the field is. Lighter on purpose so it reads as an absence.

   The exact values are measured, not picked by eye:
     --hint         #6B7860 = 4.69:1 on white. The first shade tried was 4.49:1,
                    which LOOKS identical and fails AA by a hair.
     --placeholder  #8A9680 = 3.11:1, deliberately above 3:1 so it is still
                    legible to somebody who needs the example.
   Do not lighten either without re-measuring.  */
.hint{font-size:11.5px;color:var(--hint);margin-top:6px}
/* Smaller as well as lighter, and never below 16px on a touch device — iOS
   zooms the page when you focus an input under 16px, which is the bug fixed in
   August and must not be reintroduced through a placeholder rule. */
.input::placeholder,textarea::placeholder{color:var(--placeholder);opacity:1}
.input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:var(--placeholder);opacity:1}

/* ---------- product detail ---------- */
.pdp{display:grid;gap:40px;padding:40px 0 24px}
@media(min-width:900px){.pdp{grid-template-columns:1fr 1fr;gap:56px}}
.pdp-media{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;aspect-ratio:1/1;background:var(--muted)}
.pdp-media img,.pdp-media .ph{width:100%;height:100%;object-fit:cover}
.pdp-media .ph{display:flex;align-items:center;justify-content:center;font-family:var(--font-display);letter-spacing:.16em;text-transform:uppercase;color:rgba(77,124,15,.6);background:linear-gradient(145deg,#EFF4E6,#FAFCF6)}
.pdp h1{font-size:32px;margin-top:8px}
@media(min-width:768px){.pdp h1{font-size:40px}}
.pdp-species{font-family:var(--font-mono);font-style:italic;font-size:14px;color:var(--muted-foreground);margin-top:10px}
.pdp-desc{margin-top:20px;color:var(--muted-foreground);line-height:1.7}
.pdp-price{font-family:var(--font-display);font-size:32px;font-weight:700;margin:22px 0 4px}
.ladder{margin-top:24px;display:flex;flex-direction:column;gap:10px}
.ladder__row{display:flex;align-items:center;gap:14px;border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px;cursor:pointer;transition:.2s;text-align:left;width:100%}
.ladder__row:hover{border-color:var(--primary)}
.ladder__row.is-active{border-color:var(--primary);box-shadow:0 0 10px rgba(101,163,13,.18)}
.ladder__row:disabled{opacity:.5;cursor:not-allowed}
.ladder__label{font-weight:600;font-size:14px}
.ladder__sku{font-family:var(--font-mono);font-size:11.5px;color:var(--muted-foreground);margin-top:2px}
.ladder__price{margin-left:auto;font-weight:700;font-size:15px;text-align:right}
.qty{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:6px;overflow:hidden}
.qty button{width:36px;height:38px;font-size:17px;color:var(--muted-foreground)}
.qty button:hover{color:var(--primary)}
.qty input{width:46px;height:38px;text-align:center;border:none;border-left:1px solid var(--border);border-right:1px solid var(--border);font-family:var(--font-mono);font-size:14px}
.qty input:focus{outline:none}
.pdp-actions{margin-top:24px;display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.care{margin-top:32px;border-top:1px solid var(--border);padding-top:24px}
.care h3{font-family:var(--font-display);font-size:16px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.care p{font-size:14px;color:var(--muted-foreground);line-height:1.7}

/* ---------- cart drawer ---------- */
.drawer-scrim{position:fixed;inset:0;background:rgba(22,27,16,.45);z-index:80;opacity:0;transition:opacity .25s}
.drawer-scrim.open{opacity:1}
.drawer{position:fixed;top:0;right:0;bottom:0;width:min(420px,100%);background:#fff;z-index:81;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .28s ease;border-left:1px solid var(--border)}
.drawer.open{transform:none}
.drawer__head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--border)}
.drawer__head h2{font-size:18px}
.drawer__body{flex:1;overflow-y:auto;padding:16px 24px}
.drawer__foot{border-top:1px solid var(--border);padding:20px 24px;background:var(--secondary)}
.line{display:flex;gap:14px;padding:14px 0;border-bottom:1px solid var(--border)}
.line:last-child{border-bottom:none}
.line__thumb{width:64px;height:64px;border-radius:6px;overflow:hidden;flex-shrink:0;background:var(--muted)}
.line__thumb img{width:100%;height:100%;object-fit:cover}
.line__thumb .ph{width:100%;height:100%;background:linear-gradient(145deg,#EFF4E6,#FAFCF6)}
.line__main{flex:1;min-width:0}
.line__name{font-weight:600;font-size:14px;line-height:1.35}
.line__meta{font-family:var(--font-mono);font-size:11.5px;color:var(--muted-foreground);margin-top:3px}
.line__row{display:flex;align-items:center;justify-content:space-between;margin-top:8px;gap:10px}
.line__price{font-weight:700;font-size:14px}
.line__x{font-size:12px;color:var(--muted-foreground);text-decoration:underline}
.line__x:hover{color:var(--alert)}
.totals{font-size:14px}
.totals__row{display:flex;justify-content:space-between;padding:4px 0;color:var(--muted-foreground)}
.totals__row.grand{color:var(--foreground);font-weight:700;font-size:17px;border-top:1px solid var(--border);margin-top:8px;padding-top:12px}
.ship-note{font-family:var(--font-mono);font-size:11.5px;color:var(--primary);margin-top:6px}

/* ---------- checkout ---------- */
.checkout{display:grid;gap:40px;padding:40px 0 72px}
@media(min-width:900px){.checkout{grid-template-columns:1.3fr 1fr;gap:56px;align-items:start}}
.panel{border:1px solid var(--border);border-radius:var(--radius);padding:24px;background:var(--secondary)}
.panel h3{font-family:var(--font-display);font-size:16px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:16px}
.summary-line{display:flex;justify-content:space-between;gap:12px;font-size:14px;padding:8px 0;border-bottom:1px solid var(--border)}
.summary-line:last-of-type{border-bottom:none}
.radio-row{display:flex;align-items:flex-start;gap:10px;border:1px solid var(--border);border-radius:6px;padding:12px 14px;background:#fff;cursor:pointer;margin-bottom:10px}
.radio-row.is-active{border-color:var(--primary)}
.radio-row input{margin-top:3px;accent-color:var(--primary)}
.radio-row__label{font-weight:600;font-size:14px}
.radio-row__hint{font-size:12.5px;color:var(--muted-foreground);margin-top:2px}
.error-box{background:rgba(196,86,62,.07);border:1px solid rgba(196,86,62,.35);color:var(--alert);border-radius:6px;padding:12px 14px;font-size:13.5px;margin-bottom:16px}
.error-box[hidden]{display:none}

/* ---------- account ---------- */
.account{display:grid;gap:40px;padding:48px 0 72px}
/* stretch, not start: the aside is always shorter than the form, and letting it
   end early left a white step down the right of the page — the blank the ink
   panel was introduced to close. */
@media(min-width:900px){.account{grid-template-columns:1.1fr .9fr;gap:56px;align-items:stretch}}
.auth-card{border:1px solid var(--border);border-radius:var(--radius);padding:32px;
  background:var(--surface-0);box-shadow:var(--shadow-md)}
@media(min-width:900px){.auth-card{padding:38px 36px}}
.auth-tabs{display:flex;margin-bottom:22px;border:1px solid var(--border);border-radius:6px;overflow:hidden}
.auth-tabs button{flex:1;padding:10px 14px;font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted-foreground);background:#fff;border-right:1px solid var(--border);transition:.2s}
.auth-tabs button:last-child{border-right:none}
.auth-tabs button.is-active{background:var(--primary);color:#fff}
/* "Create account" wrapped to two lines on a phone, so the two tabs were
   different heights. The card also gave up 64px of a 390px screen to its own
   padding, which is what made the label too tight in the first place. */
@media(max-width:480px){
  .auth-card{padding:22px 18px}
  .auth-tabs button{padding:11px 6px;font-size:11px;letter-spacing:.04em;white-space:nowrap}
}
.auth-aside{border:1px solid var(--border);border-radius:var(--radius);padding:24px;background:var(--secondary)}
.auth-aside h3{font-size:15px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:14px}
.auth-aside ul{list-style:none;display:flex;flex-direction:column;gap:12px}
.auth-aside li{font-size:14px;color:var(--muted-foreground);line-height:1.6;padding-left:16px;position:relative}
.auth-aside li::before{content:"";position:absolute;left:0;top:9px;width:5px;height:5px;border-radius:999px;background:var(--primary)}
.auth-aside strong{color:var(--foreground);font-weight:600}

/* The sign-in page was a white form on a white page with a pale box beside it
   and a dead corner under it — nothing on it said Exofeast. The signed-OUT
   aside becomes the same ink surface as the footer, which is where the brand
   board puts neon (§02), and carries the coiled snake in the corner it was
   leaving empty. Signed IN it stays pale: that panel holds the order list, and
   a wall of rows on ink is harder to read for no gain. */
.auth-aside--dark{background:var(--foreground);border-color:#2A331F;
  position:relative;overflow:hidden;padding:32px 30px 34px}
@media(min-width:900px){.auth-aside--dark{padding:38px 34px 40px}}
.auth-aside--dark::after{content:"";position:absolute;right:-46px;bottom:-52px;
  width:250px;height:250px;
  -webkit-mask-image:var(--art-snake);mask-image:var(--art-snake);
  background:var(--neon);opacity:.13}
@media(max-width:560px){.auth-aside--dark::after{width:180px;height:180px;right:-34px;bottom:-38px}}
.auth-aside--dark > *{position:relative;z-index:1}
.auth-aside--dark .eyebrow{color:var(--neon);opacity:.8;margin-bottom:10px}
.auth-aside--dark h3{color:#FFFFFF}
.auth-aside--dark li{color:#A8B69B}
.auth-aside--dark li::before{background:var(--neon);box-shadow:0 0 8px rgba(163,230,53,.6)}
.auth-aside--dark strong{color:#FFFFFF}
.auth-aside--dark .hint{color:#7E8D72;border-top:1px solid rgba(255,255,255,.10);
  padding-top:18px;margin-top:22px}
.linkish{display:block;width:100%;text-align:center;margin-top:14px;font-size:13px;color:var(--muted-foreground);text-decoration:underline;background:none;border:none}
.linkish:hover{color:var(--primary)}
.order-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 0;border-bottom:1px solid var(--border)}
.order-row:last-child{border-bottom:none}
.order-row:hover .order-row__no{color:var(--primary)}
.order-row__no{display:block;font-family:var(--font-mono);font-size:13px;font-weight:500}
.order-row__meta{display:block;font-size:12px;color:var(--muted-foreground);margin-top:3px}
.order-row__right{display:flex;flex-direction:column;align-items:flex-end;gap:5px}
.order-row__total{font-weight:700;font-size:14px}
/* The account control, signed in or out. Previously a lime dot plus 9px of
   mono text, which read as decoration — signed-in and signed-out looked
   near-identical, and that is the single distinction this control exists to
   make. Now a pill with a real label at both states. */
.account-chip{display:inline-flex;align-items:center;gap:8px;padding:6px 12px 6px 6px;
  border:1px solid var(--border);border-radius:999px;background:#fff;
  font-size:13px;font-weight:600;color:var(--foreground);transition:.2s;white-space:nowrap}
.account-chip:hover{border-color:var(--primary);color:var(--primary);
  box-shadow:0 0 12px rgba(101,163,13,.18)}
.account-chip__avatar{display:inline-flex;align-items:center;justify-content:center;
  width:26px;height:26px;border-radius:999px;background:var(--primary);color:#fff;
  font-family:var(--font-display);font-size:13px;font-weight:700;line-height:1;flex-shrink:0}
.account-chip__name{max-width:110px;overflow:hidden;text-overflow:ellipsis}
/* Signed out: the person glyph stands in for the avatar, same pill. */
.account-chip--out{padding:7px 14px;gap:7px}
.account-chip--out svg{width:17px;height:17px}
/* On a phone the header is 64px and the cart and menu need the room, so the
   name drops and the avatar alone carries the state. */
@media(max-width:700px){
  .account-chip__name{display:none}
  .account-chip{padding:5px;border-color:transparent}
  .account-chip--out{padding:6px}
}

/* ---------- payment box ---------- */
/* Sits directly under the order number, because "where do I send the money"
   is the only question anyone has on this page. */
.pay-box{border:1px solid var(--primary);border-radius:var(--radius);padding:22px;margin-top:24px;
  background:linear-gradient(145deg,#FDFEFB 0%,#F2F6EC 100%);text-align:center}
.pay-box h4{font-family:var(--font-display);font-size:16px;text-transform:uppercase;letter-spacing:.07em;margin-bottom:14px}
.pay-box--done{border-color:var(--border);background:var(--secondary)}
.pay-box--warn{border-color:rgba(196,86,62,.4);background:rgba(196,86,62,.05)}
.pay-qr{width:200px;height:200px;object-fit:contain;margin:0 auto 14px;border:1px solid var(--border);
  border-radius:8px;background:#fff;padding:8px}
.pay-amount{font-family:var(--font-display);font-size:24px;font-weight:700;letter-spacing:.02em;word-break:break-all}
.pay-who{font-size:13.5px;color:var(--muted-foreground);margin-top:4px;line-height:1.6}
.pay-box .btn-outline{margin-top:12px}
.pay-box .field{text-align:left;margin-top:8px}

/* ---------- order confirmation ---------- */
.receipt{max-width:720px;margin:0 auto;padding:56px 0 72px}
.receipt__mark{width:64px;height:64px;border-radius:999px;background:rgba(101,163,13,.1);border:1px solid rgba(101,163,13,.35);display:flex;align-items:center;justify-content:center;color:var(--primary);margin-bottom:24px}
.receipt__mark svg{width:30px;height:30px}
.receipt__no{font-family:var(--font-mono);font-size:14px;color:var(--primary);letter-spacing:.1em;margin-top:10px}
.timeline{margin-top:28px;border-left:2px solid var(--border);padding-left:20px}
.timeline__step{position:relative;padding-bottom:18px}
.timeline__step::before{content:"";position:absolute;left:-26px;top:4px;width:10px;height:10px;border-radius:999px;background:var(--border)}
.timeline__step.is-done::before{background:var(--primary);box-shadow:0 0 8px rgba(101,163,13,.5)}
.timeline__label{font-family:var(--font-display);font-size:14px;text-transform:uppercase;letter-spacing:.08em}
.timeline__at{font-family:var(--font-mono);font-size:11.5px;color:var(--muted-foreground);margin-top:2px}

/* ---------- testimonials ---------- */
.testi-grid{margin-top:40px;display:grid;gap:24px}
@media(min-width:768px){.testi-grid{grid-template-columns:repeat(3,1fr)}}
.testi-card{border-radius:var(--radius);border:1px solid var(--border);padding:24px}
.stars{display:flex;gap:4px}
.stars svg{width:16px;height:16px;color:var(--primary);fill:var(--primary)}
.stars svg.empty{fill:none;color:var(--border)}
.testi-quote{margin-top:16px;font-size:14px;line-height:1.65;color:var(--muted-foreground)}
.testi-name{margin-top:16px;font-size:14px;font-weight:600}

/* ---------- prose (about, care guides) ---------- */
.prose{max-width:720px;padding:8px 0 72px}
.prose h2{margin-top:40px;margin-bottom:14px;font-size:22px}
.prose p{color:var(--muted-foreground);line-height:1.8;margin-bottom:16px}
.prose ul{color:var(--muted-foreground);line-height:1.8;margin:0 0 16px 20px}
.prose strong{color:var(--foreground);font-weight:600}

/* ---------- states ---------- */
.empty{padding:56px 24px;text-align:center;color:var(--muted-foreground)}
.empty p{margin:14px 0 20px;font-size:14px}
.skel{display:grid;gap:24px;margin-top:40px}
@media(min-width:640px){.skel{grid-template-columns:1fr 1fr}}
@media(min-width:1024px){.skel{grid-template-columns:repeat(3,1fr)}}
.skel__card{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.skel__img{aspect-ratio:1/1;background:linear-gradient(90deg,#F2F5EE 25%,#FAFCF6 50%,#F2F5EE 75%);background-size:200% 100%;animation:shimmer 1.4s infinite}
.skel__line{height:12px;margin:16px;border-radius:4px;background:var(--muted)}
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
/* Text-shaped skeleton — see skeletonText() in js/ui.js. */
.skel-text{display:grid;gap:13px;padding:8px 0 24px;max-width:640px}
.skel-text__line{height:14px;border-radius:4px;
  background:linear-gradient(90deg,#F2F5EE 25%,#FAFCF6 50%,#F2F5EE 75%);
  background-size:200% 100%;animation:shimmer 1.4s infinite}
/* Announced to a screen reader, invisible to everyone else: the bars carry no
   meaning, so aria-hidden on them plus this is what keeps the state audible. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
@media (prefers-reduced-motion: reduce){
  .skel-text__line,.skel__img{animation:none}
}

/* ---------- toasts ---------- */
#toasts{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:120;display:flex;flex-direction:column;gap:10px;align-items:center;pointer-events:none}
.toast{background:var(--foreground);color:#EDF2E6;font-size:13.5px;padding:11px 18px;border-radius:999px;box-shadow:0 8px 24px rgba(22,27,16,.2);max-width:90vw}
.toast--error{background:var(--alert)}
.toast--out{opacity:0;transition:opacity .3s}

/* ---------- footer ---------- */
footer.site{border-top:1px solid var(--border);background:rgba(246,248,242,.8)}
/* LONGHAND, and it has to stay longhand.
   Both .footer-inner and .header-inner also carry .container, which supplies
   `padding:0 24px` — the gutter that keeps text off the edge of a phone. A
   `padding: 48px 0` shorthand here is a single class, same specificity as
   .container, and it comes later in the file, so it WINS and resets left and
   right to zero. On a desktop nobody notices: past 1280px the container is
   centred by its max-width and there is whitespace either side anyway. On a
   phone the footer text sits hard against the edge of the screen and the first
   letter of every line is unreadable. Reported from a phone on 2026-08-20.
   The same shorthand had done the same thing to .trust-grid on the homepage. */
.footer-inner{padding-top:48px;padding-bottom:48px}
.footer-grid{display:grid;gap:32px}
@media(min-width:768px){.footer-grid{grid-template-columns:1fr}.footer-inner{padding-top:64px;padding-bottom:64px}}
.footer-tag{margin-top:12px;font-size:14px;color:var(--muted-foreground)}
.footer-h{margin-bottom:12px;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.18em}
.footer-links{list-style:none;display:flex;flex-direction:column;gap:8px;font-size:14px;color:var(--muted-foreground)}
.footer-links a{transition:color .2s}
.footer-links a:hover{color:var(--primary)}
.footer-bottom{margin-top:40px;border-top:1px solid var(--border);padding-top:24px;text-align:center;font-size:12px;color:var(--muted-foreground)}

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{transition:none!important;animation:none!important}
}

/* ---------- static (no-JS) chrome ---------- */
/* nav.desktop is display:none below 768px because the dynamic header swaps in a
   burger menu at that width. The generated static nav has no burger to swap to,
   so it must stay visible at every width or a phone with JS off gets a header
   with no navigation in it at all. Specificity matches nav.desktop deliberately. */
nav.desktop.nav-static{display:flex;flex-wrap:wrap;gap:16px;row-gap:6px}
@media(min-width:768px){nav.desktop.nav-static{gap:32px}}
.noscript-note{font-size:13px;color:var(--muted-foreground);padding:10px 0 14px;
  border-top:1px solid var(--border);line-height:1.6}

/* ---------- logo lockup ---------- */
/* The mark with its tagline beneath. Replaces the old logo + shop-name pairing:
   the name is already in the logo, so repeating it as text said the same thing
   twice, and the tagline is the line that actually carries the brand. */
.logo-lockup{display:flex;flex-direction:column;justify-content:center;gap:2px;min-width:0}
.logo-tagline{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted-foreground);white-space:nowrap;line-height:1}
/* Hidden on narrow screens: the header is 64px there and the mark alone needs
   most of it. Reappears at the 80px desktop header. */
.logo-tagline{display:none}
@media(min-width:900px){.logo-tagline{display:block}}
/* In the footer there is no height pressure, so it always shows. */
footer.site .logo-tagline{display:block;white-space:normal;margin-top:6px}

/* ---------- home banners ---------- */
/* Editable in the backoffice Content screen. The section is hidden until there
   is at least one, so a shop that never uses them loses no vertical space. */
.banner{display:flex;flex-direction:column;gap:0;border:1px solid var(--border);
  border-radius:var(--radius);overflow:hidden;margin-bottom:18px;
  background:linear-gradient(145deg,#FDFEFB 0%,#F2F6EC 100%);transition:border-color .25s,box-shadow .25s}
.banner--link:hover{border-color:var(--primary);box-shadow:0 0 12px rgba(101,163,13,.22)}
.banner__media{overflow:hidden;max-height:260px}
.banner__media img{width:100%;height:100%;object-fit:cover;transition:transform .6s}
.banner--link:hover .banner__media img{transform:scale(1.04)}
.banner__body{padding:22px 24px}
.banner__title{font-family:var(--font-display);font-size:20px;font-weight:700;
  text-transform:uppercase;letter-spacing:.06em}
.banner__text{margin-top:8px;font-size:14.5px;color:var(--muted-foreground);line-height:1.65}
.banner__cta{display:inline-block;margin-top:14px;font-family:var(--font-display);font-size:13px;
  font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--primary)}
@media(min-width:768px){
  .banner{flex-direction:row;align-items:stretch}
  .banner__media{flex:0 0 42%;max-height:none}
  .banner__body{flex:1;display:flex;flex-direction:column;justify-content:center;padding:28px 32px}
}

/* ---------- care guides ---------- */
.guides{max-width:760px;padding:8px 0 72px}
.guide-toc{border:1px solid var(--border);border-radius:var(--radius);padding:20px 24px;
  background:var(--secondary);margin-bottom:40px}
.guide-toc ul{list-style:none;margin-top:10px;display:flex;flex-direction:column;gap:8px}
.guide-toc a{font-size:14.5px;color:var(--foreground)}
.guide-toc a:hover{color:var(--primary);text-decoration:underline}
.guide{padding-top:16px;margin-bottom:44px;scroll-margin-top:110px}
.guide__img{width:100%;border-radius:var(--radius);border:1px solid var(--border);
  margin-bottom:20px;max-height:320px;object-fit:cover}
.guide__title{font-family:var(--font-display);font-size:24px;font-weight:700;
  text-transform:uppercase;letter-spacing:.05em;margin-bottom:14px}
.guide__body p{color:var(--muted-foreground);line-height:1.8;margin-bottom:14px}
.faq-item{font-size:14px;line-height:1.75;color:var(--muted-foreground);margin-bottom:16px}
.faq-item:last-child{margin-bottom:0}
.faq-item strong{color:var(--foreground)}

/* ---------- FAQ accordion (guides page) ---------- */
.faq-list{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
.faq-q{border-bottom:1px solid var(--border);background:#fff}
.faq-q:last-child{border-bottom:none}
.faq-q summary{padding:16px 20px;cursor:pointer;font-weight:600;font-size:15px;
  list-style:none;display:flex;justify-content:space-between;gap:14px;align-items:center}
.faq-q summary::-webkit-details-marker{display:none}
.faq-q summary::after{content:"+";font-family:var(--font-display);font-size:20px;
  color:var(--primary);line-height:1;flex-shrink:0}
.faq-q[open] summary::after{content:"−"}
.faq-q summary:hover{color:var(--primary);background:var(--secondary)}
.faq-a{padding:0 20px 18px}
.faq-a p{color:var(--muted-foreground);line-height:1.75;margin-bottom:10px}
.faq-a p:last-child{margin-bottom:0}

/* ---------- motion ----------
   Content used to pop into place the instant Firestore answered, which reads as
   a page assembling itself rather than one arriving. Everything below is a
   fade with a small rise: enough to feel deliberate, short enough never to be
   in the way. All of it is disabled under prefers-reduced-motion at the bottom
   of this file. */
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes fade{from{opacity:0}to{opacity:1}}

.reveal{animation:rise .5s var(--ease) both}
/* Staggered by index so a grid resolves left-to-right instead of all at once.
   Capped at 8 — beyond that the last card feels late rather than choreographed. */
.reveal{animation-delay:calc(var(--i,0) * 60ms)}

/* Product and banner imagery fades in once decoded, so a slow photo arrives
   rather than snapping in over the placeholder. */
.prod-img img,.pdp-media img,.banner__media img,.cat-card img{
  animation:fade .45s var(--ease) both}

/* Cards lift on hover instead of only changing border colour. */
.prod-card{box-shadow:var(--shadow-sm);transition:border-color .25s var(--ease),
  box-shadow .25s var(--ease),transform .25s var(--ease)}
.prod-card:hover{border-color:var(--primary);box-shadow:var(--shadow-md);transform:translateY(-3px)}
.cat-card{box-shadow:var(--shadow-sm);transition:border-color .25s var(--ease),
  box-shadow .3s var(--ease),transform .3s var(--ease)}
.cat-card:hover{box-shadow:var(--shadow-lg);transform:translateY(-3px)}
.banner{box-shadow:var(--shadow-sm)}
.banner--link:hover{box-shadow:var(--shadow-md);transform:translateY(-2px)}
.testi-card{box-shadow:var(--shadow-sm);background:var(--surface-1)}

/* The add-to-cart confirmation. A toast alone is easy to miss when your eye is
   on the button you just pressed. */
@keyframes pop{0%{transform:scale(1)}40%{transform:scale(.94)}100%{transform:scale(1)}}
.add-btn.is-added,.btn-primary.is-added{animation:pop .35s var(--ease)}
.add-btn.is-added{background:var(--foreground);border-color:var(--foreground)}

/* Focus that matches the brand rather than the browser default. */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,summary:focus-visible{
  outline:2px solid var(--primary);outline-offset:2px;border-radius:4px}

/* ---------- dark footer ----------
   The brand board reserves neon lime #A3E635 for glows and dark surfaces (§02),
   and until now the site had no dark surface at all — so the palette was only
   ever half used and every section sat on the same white. A dark footer closes
   the page, gives the layout a base, and is the one place the neon is correct. */
footer.site{background:var(--foreground);border-top:none;color:#C9D4BC}
footer.site .footer-inner{padding-top:64px;padding-bottom:32px}
@media(min-width:768px){footer.site .footer-inner{padding-top:80px;padding-bottom:36px}}
footer.site .footer-grid{gap:40px}
/* Brand block, then the link columns as a grid of their own.
   Nesting them is what keeps Shop / Company / Contact / Follow on ONE row at
   every width: as five siblings of the wide brand block they wrapped, and
   Follow — being last — was the one that dropped, reading as an afterthought
   bolted to the bottom. Grouped, the four share a row and the brand block can
   be any width it likes. */
.footer-cols{display:grid;gap:32px;grid-template-columns:1fr}
@media(min-width:560px){.footer-cols{grid-template-columns:repeat(2,1fr)}}
/* FIVE columns now, not four — Legal joined Shop / Company / Contact / Follow
   when the privacy policy and terms were added. At 768px five would be too
   narrow beside the brand block, so they stay in two rows of two-and-three
   until there is room, and only spread onto one row at 1100px. Getting this
   wrong is what dropped Follow onto a line of its own the last time a column
   was added. */
@media(min-width:768px){
  footer.site .footer-grid{grid-template-columns:1fr 2.4fr;gap:48px}
  .footer-cols{grid-template-columns:repeat(3,1fr);gap:24px}
}
@media(min-width:1100px){
  footer.site .footer-grid{grid-template-columns:1fr 3.2fr}
  .footer-cols{grid-template-columns:repeat(5,1fr);gap:20px}
}
footer.site .footer-h{color:#FFFFFF;font-size:12px;letter-spacing:.2em;margin-bottom:16px}
footer.site .footer-links{gap:11px}
footer.site .footer-links a,footer.site .footer-links li{color:#A8B69B;font-size:14px}
footer.site .footer-links a:hover{color:var(--neon)}
footer.site .footer-tag{color:#8D9C80;font-size:14px;line-height:1.7;max-width:34ch;margin-top:14px}
footer.site .logo-fallback{color:#FFFFFF}
footer.site .logo-fallback span{color:var(--neon);text-shadow:0 0 18px rgba(163,230,53,.4)}
footer.site .logo-tagline{color:#7E8D72}
footer.site .logo-img{filter:none}

.footer-bottom{margin-top:52px;border-top:1px solid rgba(255,255,255,.09);padding-top:24px;
  display:flex;flex-wrap:wrap;gap:10px 24px;align-items:center;justify-content:space-between;
  text-align:left;font-size:12.5px;color:#7E8D72}
.footer-credit{font-family:var(--font-mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
/* The product name lifts out of the surrounding grey, but stays quieter than
   the link beside it — the vendor is the click target, so it keeps the accent. */
.footer-credit b{color:#C9D4BC;font-weight:500}
.footer-credit a{color:#A8B69B;border-bottom:1px solid rgba(163,230,53,.35);padding-bottom:1px}
.footer-credit a:hover{color:var(--neon);border-bottom-color:var(--neon)}

/* ---------- promotions ----------
   NOT the red ribbon Foyer uses. The brand board reserves Viper #C4563E for
   alerts only (§02) — low stock, failed delivery, destructive actions — so a
   red sale flag would collide with the one colour that has to mean "something
   is wrong". Forest ink with neon lime is the shop's own high-contrast pair and
   is exactly what the board reserves dark surfaces for.

   Louder than the first attempt, which was too polite to notice against dark
   product photography: bigger type, a real glow, and the card itself carries a
   lime edge so a discounted item is distinguishable before you read a price. */
.promo-tag{position:absolute;top:12px;right:12px;z-index:3;
  background:var(--foreground);color:var(--neon);
  font-family:var(--font-display);font-size:13px;font-weight:700;
  letter-spacing:.1em;text-transform:uppercase;padding:7px 13px;border-radius:5px;
  border:1px solid rgba(163,230,53,.35);
  box-shadow:0 3px 14px rgba(22,27,16,.35), 0 0 22px rgba(163,230,53,.3);
  animation:promoIn .5s var(--ease) both;animation-delay:.35s}
@keyframes promoIn{from{opacity:0;transform:translateY(-6px) scale(.9)}to{opacity:1;transform:none}}
.promo-tag--inline{position:static;display:inline-block;vertical-align:middle;margin-left:12px;animation:none}

/* A discounted card is legible as one at a glance, before any price is read. */
.prod-card.is-promo{border-color:rgba(101,163,13,.45)}
.prod-card.is-promo:hover{border-color:var(--primary);box-shadow:var(--shadow-md),0 0 18px rgba(101,163,13,.18)}

/* The old price. Muted rather than red — it is context, not a warning. */
.was{color:var(--muted-foreground);font-weight:400;opacity:.7;
  text-decoration-thickness:1px;margin-right:4px}
.prod-price .was{font-size:.78em}
.pdp-price .was{font-size:.55em}
.ladder__price .was{font-size:.78em;display:inline-block}
.prod-price .now{font-weight:700}
/* The saving in pesos. "20% OFF" is an abstraction; this is the number someone
   actually decides on, so it gets its own chip rather than being implied. */
.save{display:inline-block;margin-left:8px;padding:3px 9px;border-radius:999px;
  background:rgba(101,163,13,.12);border:1px solid rgba(101,163,13,.3);
  color:var(--primary);font-family:var(--font-mono);font-size:10.5px;
  font-weight:500;letter-spacing:.06em;text-transform:uppercase;vertical-align:middle}
@media(max-width:420px){.save{display:none}}

/* ==========================================================================
   LINE ART
   Minimal exotic-animal drawings used to hold the page where a section would
   otherwise trail off into white — the empty half beside a page title, the
   dead corner under the sign-in aside, the bottom of the footer.

   They are applied as CSS *masks*, not background-images. A mask takes its
   colour from `background`, so one file serves both colourways — ink at 6% on
   white, neon at 14% on the dark footer — instead of shipping a light and a
   dark copy of every drawing and keeping the pair in sync.

   All of it is decoration: every rule below is on a pseudo-element, so nothing
   here can be read by a screen reader, selected, or clicked. If a drawing ever
   needs to carry meaning it must become a real <img> with alt text instead.
   ========================================================================== */
:root{
  --art-snake:url(/images/art/snake.svg);
  --art-gecko:url(/images/art/gecko.svg);
  --art-spider:url(/images/art/spider.svg);
  --art-serpent:url(/images/art/serpent.svg);
}

/* mask-mode is set explicitly: the `mask` shorthand resets it, and engines
   disagree on what `match-source` means for an SVG file. The art is drawn in
   opaque white so alpha and luminance both resolve to "show it". */
.art,
.page-head::after,
.auth-aside--dark::after,
.empty::before,
footer.site::after{
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:contain;mask-size:contain;
  mask-mode:alpha;
  pointer-events:none;
}

/* ---------- page headings ----------
   A page title takes the left half of a wide screen and nothing took the right,
   which read as an unfinished layout rather than as space. Faint enough to be
   texture: if you notice it before you read the heading it is too strong. */
.page-head{position:relative}
.page-head::after{content:"";position:absolute;display:none;
  right:6px;top:50%;transform:translateY(-50%);width:210px;height:210px;
  background:var(--foreground);opacity:.06}
/* The heading becomes a band tall enough to hold the drawing, rather than the
   drawing hanging out of it over the filter row below. min-height is what keeps
   the two from colliding — without it the art overlaps the search box, which is
   positioned later in the DOM and would sit under it. */
@media(min-width:1000px){
  .page-head[data-art]{min-height:236px;padding-right:270px;
    display:flex;flex-direction:column;justify-content:center}
  .page-head[data-art]::after{display:block}
}
.page-head[data-art="gecko"]::after{-webkit-mask-image:var(--art-gecko);mask-image:var(--art-gecko)}
.page-head[data-art="snake"]::after{-webkit-mask-image:var(--art-snake);mask-image:var(--art-snake)}
.page-head[data-art="spider"]::after{-webkit-mask-image:var(--art-spider);mask-image:var(--art-spider)}
/* Nothing to draw unless a page asked for a creature by name. */
.page-head:not([data-art])::after{content:none}

/* ---------- empty states ----------
   An empty result set is the one moment the shop has nothing to show, which is
   exactly when a bare line of grey text feels broken rather than intentional. */
/* An empty state dropped into a product grid is still a grid item, so it lands
   in column one and reads as a stray card. Span it here rather than asking each
   caller to remember an inline grid-column. */
.prod-grid > .empty{grid-column:1/-1}
.empty{position:relative}
.empty::before{content:"";display:block;width:84px;height:84px;margin:0 auto 20px;
  -webkit-mask-image:var(--art-snake);mask-image:var(--art-snake);
  background:var(--muted-foreground);opacity:.3}
.empty[data-art="gecko"]::before{-webkit-mask-image:var(--art-gecko);mask-image:var(--art-gecko)}
.empty[data-art="spider"]::before{-webkit-mask-image:var(--art-spider);mask-image:var(--art-spider)}

/* ---------- footer ----------
   The columns are ragged by nature — Shop has three links, Company has six — so
   the bottom right of the footer is always short. The serpent runs under the
   whole width and gives the block a floor. */
footer.site{position:relative;overflow:hidden}
footer.site::after{content:"";position:absolute;z-index:0;
  left:0;right:0;bottom:4px;height:104px;
  -webkit-mask-image:var(--art-serpent);mask-image:var(--art-serpent);
  -webkit-mask-position:left bottom;mask-position:left bottom;
  -webkit-mask-size:auto 100%;mask-size:auto 100%;
  background:var(--neon);opacity:.07}
@media(max-width:767px){footer.site::after{height:64px;opacity:.05}}
footer.site .footer-inner{position:relative;z-index:1}

/* ---------- contact form: the confirmation that replaces it ---------- */
.form-note{margin-top:12px;font-size:13px;line-height:1.6;padding:10px 12px;border-radius:8px}
.form-note--bad{color:var(--alert);background:rgba(196,86,62,.07);border:1px solid rgba(196,86,62,.3)}
.form-note[hidden]{display:none}
.sent{text-align:center;padding:22px 6px}
.sent__mark{width:44px;height:44px;margin:0 auto 14px;border-radius:999px;display:flex;align-items:center;
  justify-content:center;background:var(--primary);color:#fff;font-size:22px;line-height:1}
.sent h4{font-family:var(--font-display);font-size:17px;text-transform:uppercase;letter-spacing:.05em}
.sent p{margin-top:8px;font-size:14px;line-height:1.65;color:var(--muted-foreground)}

/* ---------- social ---------- */
/* Footer list: the handle carries the identity, the mark carries the
   recognition. An icon on its own is a guess about which account it is. */
.footer-social{list-style:none;display:flex;flex-direction:column;gap:9px}
.footer-social a{display:inline-flex;align-items:center;gap:9px;font-size:14px;opacity:.85;transition:opacity .18s,color .18s}
.footer-social a:hover{opacity:1;color:var(--neon)}
.footer-social svg{flex:none}

/* The CTA. Lime is scarce in this brand (§01), so the buttons carry it and the
   panel stays Mist — the section asks without shouting over the catalogue. */
.social-cta{padding-top:8px}
/* Tight variant, used on the contact page.
   The default 96px belongs to the homepage, where the CTA follows other
   full-bleed sections and shares their rhythm. Contact ends with two panels
   20px apart, so the same 96px read as a different page bolted underneath —
   which is exactly what it looked like. Enough to separate, not to disown. */
/* `section.` on purpose: the rule it has to beat is `section.block{padding:96px 0}`,
   which at (0,1,1) outranks a bare class. Matching its specificity and sitting
   later in the file is what makes this win — the same shape as the display trap
   documented at the top of admin/css/app.css. */
section.social-cta--tight{padding-top:34px;padding-bottom:52px}
/* The wrapper above it carries 72px of its own bottom padding — sensible when
   main content ran straight into the footer, redundant now that a section
   follows. Left in place, the two stacked to 106px and the Follow block read as
   a different page. */
.checkout--flush{padding-bottom:8px}
@media(max-width:767px){section.social-cta--tight{padding-top:26px;padding-bottom:38px}}
.social-cta__inner{display:grid;gap:28px;grid-template-columns:1fr;align-items:center;
  background:var(--muted);border:1px solid var(--border);border-radius:var(--radius);padding:34px}
@media(min-width:860px){.social-cta__inner{grid-template-columns:1.25fr 1fr;gap:44px}}
.social-cta h2{margin-top:8px}
.social-cta__blurb{margin-top:12px;color:var(--muted-foreground);font-size:15px;line-height:1.7;max-width:52ch}
.social-cta__links{display:flex;flex-direction:column;gap:12px}
.social-btn{display:flex;align-items:center;gap:13px;padding:14px 18px;border-radius:var(--radius);
  border:1px solid var(--border);background:#fff;color:var(--foreground);
  transition:border-color .18s,box-shadow .18s,transform .18s}
.social-btn:hover{border-color:var(--primary);box-shadow:var(--shadow-md);transform:translateY(-2px)}
.social-btn svg{flex:none;color:var(--primary)}
.social-btn span{display:flex;flex-direction:column;line-height:1.35}
.social-btn strong{font-size:14.5px}
.social-btn em{font-style:normal;font-family:var(--font-mono);font-size:11.5px;color:var(--muted-foreground)}

/* ---------- care guides: index cards and a single guide ---------- */
.guide--card{border-bottom:1px solid var(--border);padding-bottom:22px}
.guide--card .guide__title a{color:inherit;text-decoration:none}
.guide--card .guide__title a:hover{color:var(--primary)}
.guide__excerpt{margin-top:10px;color:var(--muted-foreground);line-height:1.7;max-width:62ch}
.guide__more{display:inline-block;margin-top:10px;color:var(--primary);font-weight:600}
.guide-crumbs{font-family:var(--font-mono);font-size:11px;letter-spacing:.06em;color:var(--muted-foreground);margin-bottom:18px}
.guide-crumbs a{color:var(--muted-foreground)}
.guide-crumbs a:hover{color:var(--primary)}
.guide-more{list-style:none;display:grid;gap:14px;margin-top:12px}
.guide-more li a{font-weight:600}
.guide-more li span{display:block;color:var(--muted-foreground);font-size:14px;line-height:1.6;margin-top:3px}
.guide-back{margin-top:28px;padding-top:18px;border-top:1px solid var(--border);color:var(--muted-foreground)}
.guide-back a{color:var(--primary)}

/* A row of buttons that wraps like a row rather than like text.
   Two inline-flex buttons inside a <p> separated by a margin look fine in one
   line and collide in two: wrapped, the only thing between them vertically is
   the paragraph's line-height, which is nothing next to a 44px button. Reported
   from a phone on 2026-08-20 as "almost overlapping". `gap` handles both axes,
   which a margin cannot. */
.btn-row{display:flex;flex-wrap:wrap;gap:12px;align-items:center}

/* Only ever ONE toast on screen — see toast() in ui.js. A repeat of the same
   message refreshes the one already there, and the pulse is the entire signal
   that it fired again: without it a second tap on a sold-out size looks like
   nothing happened, which invites a third. There is deliberately no "(2x)"
   counter — on a shop where every other number is a quantity, it read as one.
   Suppressed under prefers-reduced-motion; the message itself is unchanged. */
.toast--again{animation:toastAgain .32s ease}
@keyframes toastAgain{
  0%{transform:scale(1)}
  40%{transform:scale(1.06)}
  100%{transform:scale(1)}
}
@media (prefers-reduced-motion: reduce){
  .toast--again{animation:none!important}
}

/* --- touch: stop the two things that zoom a phone unasked ----------------- */

/* 1. Double-tap zoom.
   `manipulation` tells the browser a touch starting here is for panning or
   pinching, never for double-tap-to-zoom. PINCH ZOOM STILL WORKS, which is the
   whole point: the alternative fix people reach for is user-scalable=no in the
   viewport meta, and that takes pinch away too — locking out anyone who needs
   to magnify. Never add that; the pre-deploy checklist forbids it explicitly. */
html { touch-action: manipulation; }

/* 2. iOS zooms in when you focus ANY input whose font is under 16px, and every
   field here was 13.5–14px. Tapping the search box or a checkout field jumped
   the whole page, which reads as the same bug as double-tap and is worse,
   because it happens mid-order.
   Scoped to coarse pointers so the desktop type scale is untouched — this is
   about what a finger does, not about screen width. */
@media (pointer: coarse) {
  input, select, textarea, .input { font-size: 16px; }
}

/* ---------- order-update bell (signed-in customers) ---------- */
.notif-btn{position:relative}
.notif-dot{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;
  border-radius:999px;background:var(--alert);color:#fff;font-family:var(--font-mono);
  font-size:9.5px;line-height:16px;text-align:center;font-weight:700}
.notif-panel{position:absolute;top:calc(100% + 10px);right:0;z-index:70;width:266px;
  background:var(--background);border:1px solid var(--border);border-radius:var(--radius);
  box-shadow:0 14px 34px rgba(22,27,16,.14);padding:12px 0;overflow:hidden}
.notif-panel__head{font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted-foreground);padding:0 14px 8px;margin:0}
.notif-panel__empty,.notif-panel__foot{padding:4px 14px 0;margin:0;font-size:12.5px}
.notif-panel__empty{color:var(--hint)}
.notif-list{list-style:none;margin:0;padding:0}
.notif-list a{display:flex;justify-content:space-between;align-items:center;gap:10px;
  padding:9px 14px;text-decoration:none;color:var(--foreground)}
.notif-list a:hover{background:var(--muted)}
.notif-list__no{font-family:var(--font-mono);font-size:12px}
.notif-list__status{font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--muted-foreground)}
/* An unread row is marked by a rail, not by colouring the text — the status
   words already carry meaning and recolouring them would compete with it. */
.notif-list li.is-new a{box-shadow:inset 2px 0 0 var(--primary)}
.notif-list li.is-new .notif-list__status{color:var(--primary)}
/* The header actions are the positioning context for the panel. */
.header-actions{position:relative}

/* --- Google sign-in ------------------------------------------------------
   Google's own brand guidance: their mark, unmodified, on white or on their
   blue, with the wordmark left alone. A lime Exofeast button with a G in it
   would be off-brand for them and less trustworthy for the customer, who is
   about to hand over an identity and is looking for something they recognise. */
.btn-social{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;
  min-height:48px;padding:12px 16px;border:1px solid var(--border);border-radius:var(--radius);
  background:#FFFFFF;color:#161B10;font-family:var(--font-body);font-size:15px;font-weight:600;
  cursor:pointer;transition:background .15s ease,border-color .15s ease,opacity .15s ease}
.btn-social + .btn-social{margin-top:10px}
.btn-social:hover:not(:disabled){background:#F6F8F2;border-color:var(--hint)}
.btn-social:disabled{opacity:.6;cursor:default}
.btn-social svg{flex:0 0 18px}

/* Facebook's own blue and mark. Both providers keep their own identity rather
   than being restyled in Exofeast lime: the customer is about to hand over an
   identity and is looking for the button they recognise, not the one that
   matches the shop. */
.btn-social--facebook{background:#1877F2;border-color:#1877F2;color:#FFFFFF}
.btn-social--facebook:hover:not(:disabled){background:#166FE5;border-color:#166FE5}

/* A rule with the word sitting in it, rather than a bare line — the two paths
   are alternatives, and a plain divider reads as "and then". */
.auth-or{display:flex;align-items:center;gap:12px;margin:16px 0;
  font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--muted-foreground)}
.auth-or::before,.auth-or::after{content:"";flex:1;height:1px;background:var(--border)}
