/* ---------------------------------------------------------------------------
 * Shared chrome for every public page type: the site header (identical on the
 * static landing, the static legal/about pages and the server-rendered SEO
 * pages) plus the /search page.
 *
 * Loaded from: index.html, about.html, contact.html, privacy.html, terms.html,
 * affiliate-disclosure.html, account-deletion.html, seo/views/layout.php.
 * The header MARKUP is duplicated in those files (static pages have no PHP
 * includes) — keep it in sync; the styling lives only here.
 *
 * CHANGING THIS FILE MEANS BUMPING ?v= IN THE SEVEN STATIC PAGES. The PHP
 * layout versions the link from filemtime, but a .html file cannot, so its
 * link carries the number by hand. Get it wrong and a returning visitor loads
 * the new markup against the cached old stylesheet — where the tokens below do
 * not exist yet, so every var() resolves to nothing and the page renders
 * unstyled. Nothing warns you; the pages simply look broken to the people who
 * have been here before and correct to you.
 *
 *   V=$(stat -f %m backend/assets/site.css)
 *   perl -pi -e "s{site\.css(\?v=\d+)?}{site.css?v=$V}g" backend/*.html
 * ------------------------------------------------------------------------- */

/* ===== Outfit, served from our own origin =====
 *
 * Was `fonts.googleapis.com/css2?family=Outfit`, and that cost two things
 * measured on 2026-08-05 (Lighthouse, desktop, /fragrance/light-blue-dolce-gabbana):
 *
 *   - CLS 0.163, of which 0.158 is a single shift Lighthouse attributes to
 *     "Web font loaded" on outfit v15 latin. The page painted in system-ui,
 *     the real font arrived, every block below the fold moved. This is why
 *     desktop measured 0.358 in the audit and mobile 0.01 — under mobile
 *     throttling the font wins the race against first paint, on desktop it
 *     loses it.
 *   - 190 ms of render-blocking on every page, plus a DNS+TLS handshake to a
 *     second origin before the browser even learns which woff2 it needs.
 *
 * Self-hosting collapses that chain: the <link rel="preload"> in the document
 * head starts the font on the same connection that is already fetching the
 * HTML, so it is normally there before first paint and there is nothing to
 * swap. Keep the preload in sync with the latin file below — a renamed file
 * with a stale preload is worse than no preload at all.
 *
 * One variable file per subset (weight 100–900), not five static weights: the
 * page uses 300/400/500/600/700 and the variable latin subset is 32 KB against
 * roughly 80 KB for the five.
 *
 * Files come from Google's own v15 CDN payload, unmodified. Outfit is OFL —
 * assets/fonts/OFL.txt ships with them because the licence requires it.
 */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/assets/fonts/outfit-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Brand names carry the extended Latin (Lancôme is latin, Frapin's neighbours
   in the A–Z are not). Separate subset, so it is fetched only on the pages that
   actually contain one of these characters — and never preloaded. */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(/assets/fonts/outfit-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ============================================================================
 * TOKENS
 *
 * Every colour on the public site resolves through one of these. Nothing below
 * this block, and nothing in a template's style attribute, should carry a hex
 * literal: a literal is a colour that cannot follow the theme, and it shows up
 * as one pale card in a dark page.
 *
 * Values are the dark-theme handoff in context_base/design/dark-theme/README.md,
 * direction 1a — the same warm cream-and-terracotta identity ported onto warm
 * charcoal. Lightness inverts; the hue does not, so the ground goes brown-black
 * rather than neutral grey. Depth is carried by surface lightness, not shadow.
 *
 * THREE THINGS THAT DELIBERATELY DO NOT INVERT:
 *
 *   --photo   Product shots are cut out on white, baked into the JPEG. Give the
 *             photo box any other ground and the picture's own white shows as a
 *             rectangle inside it. So the photo box stays white in both themes,
 *             which also keeps `mix-blend-mode:multiply` working on the
 *             placeholder glyphs — that blend disappears entirely on a dark
 *             ground.
 *   --acc     The terracotta fill reads on both grounds and is the brand, so it
 *             is the one colour with a single value. Terracotta as TEXT is a
 *             different token (--acc-ink): #8A5A40 has no contrast on charcoal.
 *   --on-acc  Text sitting on that fill is near-white in both themes.
 *
 * The viewer's own preference wins over the OS in both directions — the toggle
 * stamps data-theme on <html>, and those two rules come last on purpose.
 * ========================================================================== */
:root {
  color-scheme: light;

  --bg:      #F7F2EA;   /* page ground */
  /* The same ground as three numbers, for the one place that needs it at
     partial opacity: the landing hero is a photograph read through a veil of
     the page colour, and a six-stop gradient cannot be written with --bg. */
  --bg-rgb:  247,242,234;
  --card:    #FFFFFF;   /* a card, a form, anything raised off the page */
  --surf:    #FBF7F0;   /* second surface: bands, wells, the note card */
  --chip:    #F2E7DD;   /* warm chip / thumbnail ground */
  --line:    #ECE3D5;   /* default border and divider */
  --line-2:  #E6DCCD;   /* border on something interactive */
  --hair:    #F0E8DC;   /* the faintest divider we draw */

  --ink:     #20201C;   /* primary text */
  --ink-2:   #6A6157;   /* secondary text */
  --ink-3:   #3F3A33;   /* text that needs to sit above secondary but below primary */
  --mute:    #8A8073;   /* captions, stamps, counts */

  --acc:     #B5704F;   /* terracotta, as a fill */
  --acc-ink: #8A5A40;   /* terracotta, as text or an icon */
  --acc-h:   #7A4A32;   /* the same, hovered */
  --acc-e:   #D8B79E;   /* terracotta edge on hover */
  --on-acc:  #FBF5EC;   /* text on a terracotta or ink fill */

  --btn:     #1A1714;   /* the primary button's fill */
  --btn-h:   #000000;

  --photo:   #FFFFFF;   /* the ground a product photograph sits on */
  /* The cream version of the same idea, for the boxes that fall back to
     icon-bottle.svg instead of showing a photo. Fixed in both themes: the
     glyph paints its own cream canvas and multiplies into this one, and
     multiply on a dark ground erases it. */
  --photo-soft: #F2E7DD;
  --ph-edge: rgba(70,50,35,.09);
  /* How a photo box is dressed — see the .shot rule far below. All three are
     inert here, because a white plate on cream needs no help. In the dark theme
     they are what stops the same plate reading as a hole cut in the page. */
  --shot-ring: transparent;
  --shot-lift: none;
  --shot-film: transparent;

  --hdr:     rgba(247,242,234,0.86);
  --veil:    rgba(247,242,234,0.94);

  --sh-sm:   0 6px 18px rgba(70,50,35,.06);
  --sh-md:   0 8px 22px rgba(70,50,35,.07);
  --sh-lg:   0 10px 30px rgba(70,50,35,.07);
  --sh-xl:   0 16px 34px rgba(70,50,35,.10);
  --sh-menu: 0 18px 32px rgba(70,50,35,0.10);
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --bg:      #211C17;
    --bg-rgb:  33,28,23;
    --card:    #2A241E;
    --surf:    #2E2720;
    --chip:    #2E2720;
    --line:    #362E26;
    --line-2:  #3A322A;
    --hair:    #322A22;

    --ink:     #F1EADD;
    --ink-2:   #C9BEAC;
    --ink-3:   #DED5C6;
    --mute:    #9A9082;

    --acc:     #B5704F;
    --acc-ink: #C77F52;
    --acc-h:   #D9946B;
    --acc-e:   #8A5A40;
    --on-acc:  #FBF5EC;

    --btn:     #B5704F;
    --btn-h:   #C77F52;

    --photo:   #FFFFFF;
    --photo-soft: #F2E7DD;
    --ph-edge: rgba(0,0,0,.25);
    --shot-ring: rgba(0,0,0,.34);
    --shot-lift: 0 10px 26px rgba(0,0,0,.42);
    --shot-film: rgba(33,28,23,.14);

    --hdr:     rgba(33,28,23,0.86);
    --veil:    rgba(33,28,23,0.94);

    --sh-sm:   0 6px 18px rgba(0,0,0,.28);
    --sh-md:   0 8px 22px rgba(0,0,0,.30);
    --sh-lg:   0 10px 30px rgba(0,0,0,.35);
    --sh-xl:   0 16px 34px rgba(0,0,0,.45);
    --sh-menu: 0 18px 32px rgba(0,0,0,.45);
  }
}

/* The toggle's choice, and it beats the OS in BOTH directions — a viewer who
   picks light on a dark machine has to get light. Duplicated rather than
   generated because there is no way to say "the other block's values" in CSS. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:#211C17; --bg-rgb:33,28,23; --card:#2A241E; --surf:#2E2720; --chip:#2E2720;
  --line:#362E26; --line-2:#3A322A; --hair:#322A22;
  --ink:#F1EADD; --ink-2:#C9BEAC; --ink-3:#DED5C6; --mute:#9A9082;
  --acc:#B5704F; --acc-ink:#C77F52; --acc-h:#D9946B; --acc-e:#8A5A40; --on-acc:#FBF5EC;
  --btn:#B5704F; --btn-h:#C77F52;
  --photo:#FFFFFF; --photo-soft:#F2E7DD; --ph-edge:rgba(0,0,0,.25);
  --shot-ring:rgba(0,0,0,.34); --shot-lift:0 10px 26px rgba(0,0,0,.42); --shot-film:rgba(33,28,23,.14);
  --hdr:rgba(33,28,23,0.86); --veil:rgba(33,28,23,0.94);
  --sh-sm:0 6px 18px rgba(0,0,0,.28); --sh-md:0 8px 22px rgba(0,0,0,.30);
  --sh-lg:0 10px 30px rgba(0,0,0,.35); --sh-xl:0 16px 34px rgba(0,0,0,.45);
  --sh-menu:0 18px 32px rgba(0,0,0,.45);
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg:#F7F2EA; --bg-rgb:247,242,234; --card:#FFFFFF; --surf:#FBF7F0; --chip:#F2E7DD;
  --line:#ECE3D5; --line-2:#E6DCCD; --hair:#F0E8DC;
  --ink:#20201C; --ink-2:#6A6157; --ink-3:#3F3A33; --mute:#8A8073;
  --acc:#B5704F; --acc-ink:#8A5A40; --acc-h:#7A4A32; --acc-e:#D8B79E; --on-acc:#FBF5EC;
  --btn:#1A1714; --btn-h:#000000;
  --photo:#FFFFFF; --photo-soft:#F2E7DD; --ph-edge:rgba(70,50,35,.09);
  --shot-ring:transparent; --shot-lift:none; --shot-film:transparent;
  --hdr:rgba(247,242,234,0.86); --veil:rgba(247,242,234,0.94);
  --sh-sm:0 6px 18px rgba(70,50,35,.06); --sh-md:0 8px 22px rgba(70,50,35,.07);
  --sh-lg:0 10px 30px rgba(70,50,35,.07); --sh-xl:0 16px 34px rgba(70,50,35,.10);
  --sh-menu:0 18px 32px rgba(70,50,35,0.10);
}

/* The page ground itself. Every template sets its own body background inline
   today; this is the floor under them and the one the <html> element paints
   during overscroll, which is what you see bouncing at the top of a phone. */
html { background:var(--bg); }

/* ===== Site header ===== */
.site-hdr { position:sticky; top:0; z-index:60; background:var(--hdr); backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px); border-bottom:1px solid var(--line); }
.site-hdr .in { max-width:1180px; margin:0 auto; padding:14px clamp(16px,5vw,48px); display:flex; align-items:center; justify-content:space-between; gap:16px; }
.site-hdr .wordmark { display:flex; align-items:center; gap:11px; flex-shrink:0; }
.site-hdr .wordmark .dot { width:7px; height:7px; border-radius:50%; background:var(--acc); flex-shrink:0; }
.site-hdr .wordmark .name { font-size:17px; font-weight:600; letter-spacing:0.28em; text-transform:uppercase; color:var(--ink); }
/* margin-left:auto rather than relying on the row's space-between: with the
   theme switch there are three in-flow children, and space-between would have
   parked the nav in the middle of the bar. The auto margin swallows the free
   space so the nav and the switch stay together on the right. */
.site-hdr nav { display:flex; align-items:center; gap:clamp(12px,2.4vw,30px); margin-left:auto; }
.site-hdr nav a { font-size:14px; font-weight:500; color:var(--ink-2); transition:color .2s ease; }
.site-hdr nav a:hover { color:var(--ink); }
.site-hdr nav a.cta { font-weight:600; color:var(--on-acc); background:var(--btn); padding:10px 20px; border-radius:100px; transition:background .2s ease, transform .2s ease, box-shadow .2s ease; }
.site-hdr nav a.cta:hover { background:var(--btn-h); color:var(--on-acc); transform:translateY(-1px); box-shadow:0 8px 20px rgba(26,23,20,0.22); }
.site-hdr .nav-toggle { position:absolute; opacity:0; pointer-events:none; }
.site-hdr .burger { display:none; }

/* The theme switch.
   A button rather than a checkbox: it has three meanings (follow the system,
   force light, force dark) and only one of them is "unchecked". It ships with
   no icon of its own — the markup carries both glyphs and CSS shows the one
   that is NOT current, so the control reads as "what you will get", which is
   what a person reaching for it is looking for. */
.thm { display:inline-flex; align-items:center; justify-content:center; width:38px; height:38px;
  flex-shrink:0; border:1px solid var(--line-2); border-radius:100px; background:var(--card);
  color:var(--ink-2); cursor:pointer; font:inherit; padding:0;
  transition:color .2s ease, border-color .2s ease, background .2s ease; }
.thm:hover { color:var(--acc-ink); border-color:var(--acc); }
.thm:focus-visible { outline:2px solid var(--acc); outline-offset:2px; }
.thm svg { width:17px; height:17px; display:block; }
/* Light is showing, so offer the moon. */
.thm .i-moon { display:block; }
.thm .i-sun  { display:none; }
@media (prefers-color-scheme: dark) {
  .thm .i-moon { display:none; }
  .thm .i-sun  { display:block; }
}
:root[data-theme="dark"] .thm .i-moon { display:none; }
:root[data-theme="dark"] .thm .i-sun  { display:block; }
:root[data-theme="light"] .thm .i-moon { display:block; }
:root[data-theme="light"] .thm .i-sun  { display:none; }

@media (max-width:1000px) {
  .site-hdr .burger { display:flex; align-items:center; justify-content:center; width:42px; height:42px; margin-right:-8px; border-radius:12px; color:var(--ink-3); cursor:pointer; }
  .site-hdr .burger:hover { background:var(--chip); }
  .site-hdr nav { display:none; position:absolute; left:0; right:0; top:100%; flex-direction:column; align-items:stretch; gap:0; padding:6px clamp(16px,5vw,48px) 18px; background:var(--bg); border-bottom:1px solid var(--line); box-shadow:var(--sh-menu); }
  .site-hdr .nav-toggle:checked ~ nav { display:flex; }
  .site-hdr nav a { font-size:15px; padding:14px 2px; border-bottom:1px solid var(--line); }
  .site-hdr nav a.cta { margin-top:16px; padding:14px 20px; text-align:center; border-bottom:none; }
  /* The switch stays on the bar with the burger, not inside the folded menu:
     it is chrome, and a person changing it is not navigating. It sits before
     the burger, which the DOM does not — the burger has to be the last thing
     on the row for its negative margin to pull it flush with the page edge. */
  .site-hdr .thm { order:1; margin-left:auto; margin-right:4px; }
  .site-hdr .burger { order:2; }
}

/* ============================================================================
 * "How it works" — the three numbered steps on the landing page.
 *
 * Here rather than in style attributes because the phone gets a different
 * layout from the desktop, and a style attribute cannot hold a media query.
 * ========================================================================== */
.step { background:var(--card); border:1px solid var(--line); border-radius:22px;
  padding:32px 28px; box-shadow:var(--sh-sm); }
.step .n { width:46px; height:46px; border-radius:14px; background:var(--chip);
  color:var(--acc-ink); display:flex; align-items:center; justify-content:center;
  font-size:17px; font-weight:600; margin-bottom:22px; }
.step h3 { font-size:19px; font-weight:600; margin:0 0 9px; letter-spacing:-.01em; }
.step p  { font-size:14.5px; line-height:1.55; color:var(--ink-2); margin:0; }

@media (max-width:640px) {
  /* The number moves up onto the title's line. Stacked above it, it spends a
     whole row per card — 46px and its margin — to say "1", and leaves the width
     beside it empty, which on a phone is the dimension there is none of. The
     three cards are also the only thing between the search box and the results
     the page is trying to show, so their height is what the reader pays to get
     past them. */
  .step { display:grid; grid-template-columns:auto 1fr; column-gap:14px;
    align-items:center; padding:18px 18px 20px; }
  .step .n { grid-row:1; width:38px; height:38px; border-radius:12px;
    font-size:15px; margin-bottom:0; }
  .step h3 { grid-column:2; margin:0; font-size:17px; }
  .step p  { grid-column:2; margin-top:10px; }
}

/* ===== /search ===== */
.srch { max-width:720px; margin:0 auto; padding:clamp(16px,3vw,28px) clamp(16px,5vw,32px) clamp(36px,6vw,64px); }
.srch-eyebrow { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:0.16em; color:var(--mute); }
.srch-h1 { font-size:clamp(27px,4.6vw,40px); font-weight:600; letter-spacing:-0.025em; line-height:1.1; margin:14px 0 12px; color:var(--ink); }
.srch-lede { font-size:clamp(15px,2vw,17px); line-height:1.6; color:var(--ink-2); margin:0 0 22px; }
.srch-caption { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:0.14em; color:var(--mute); margin:0 0 12px; }

/* Result intro — server-printed when cached, otherwise faded in by search.js.
   Deliberately NOT styled like a result card (raised + shadow): it is editorial
   copy about the list, not an item in it, so it gets a warm tint and an accent
   edge instead of depth. */
.srch-intro { position:relative; margin:0 0 22px; padding:15px 18px 15px 19px; background:var(--surf); border:1px solid var(--line); border-left:3px solid var(--acc); border-radius:20px; font-size:clamp(14.5px,1.8vw,15.5px); line-height:1.65; color:var(--ink-2); }
.srch-intro[hidden] { display:none; }
.srch-intro#srch-intro { opacity:0; transform:translateY(4px); transition:opacity .45s ease, transform .45s ease; }
.srch-intro#srch-intro.in { opacity:1; transform:none; }
@media (prefers-reduced-motion:reduce) { .srch-intro#srch-intro { transition:none; transform:none; } }

/* query bar (entry + refine) */
.srch-form { display:flex; align-items:center; gap:10px; background:var(--card); border:1px solid var(--line-2); border-radius:16px; padding:8px 8px 8px 16px; box-shadow:var(--sh-lg); }
.srch-form:focus-within { border-color:var(--acc); }
.srch-form input[type=search] { flex:1; min-width:0; border:none; outline:none; background:transparent; font-family:inherit; font-size:16px; color:var(--ink); -webkit-appearance:none; appearance:none; }
.srch-form input[type=search]::placeholder { color:var(--mute); font-style:italic; }
.srch-form input[type=search]::-webkit-search-cancel-button { -webkit-appearance:none; }
.srch-form button { flex-shrink:0; border:none; cursor:pointer; font-family:inherit; font-size:14.5px; font-weight:600; color:var(--on-acc); background:linear-gradient(165deg,#bd7853,#9a5a3d 70%,#8a4f36); padding:11px 20px; border-radius:12px; box-shadow:0 8px 18px rgba(181,112,79,0.30); transition:filter .2s ease, box-shadow .2s ease; }
.srch-form button:hover { filter:brightness(1.05); box-shadow:0 10px 24px rgba(181,112,79,0.40); }

/* chips (presets + quick refinements) — <button type="button"> now, not <a>: they fill
   the search field via assets/search.js instead of navigating. This reset undoes native
   button chrome (font, cursor, margin, appearance) so they still render as plain pills;
   .chip/.cat get the same reset here even though their box styling lives inline in
   index.html, since site.css is the stylesheet common to every page these appear on. */
.srch-chips button, .chip, .cat { font:inherit; text-align:inherit; cursor:pointer; margin:0; -webkit-appearance:none; appearance:none; }
.srch-chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px; }
.srch-chips button { font-size:13px; font-weight:500; color:var(--acc-ink); background:var(--chip); border:1px solid transparent; padding:8px 15px; border-radius:100px; transition:background .2s ease, color .2s ease; }
.srch-chips button:hover { background:var(--line); color:var(--acc-h); }

/* results header */
.srch-bar { display:flex; align-items:center; gap:10px; max-width:720px; margin:0 auto; padding:10px clamp(16px,5vw,32px) 0; }
.srch-back { display:flex; align-items:center; justify-content:center; width:40px; height:40px; flex-shrink:0; border-radius:12px; color:var(--ink-3); }
.srch-back:hover { background:var(--chip); color:var(--ink); }
.srch-bar-mid { flex:1; min-width:0; text-align:center; }
.srch-query { display:block; font-size:15px; font-style:italic; color:var(--ink); margin-top:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* refine block, above the results */
.srch-refine { max-width:720px; margin:0 auto; padding:clamp(14px,2.5vw,20px) clamp(16px,5vw,32px) 0; }

/* filter disclosure */
.srch-filters { max-width:720px; margin:0 auto; padding:10px clamp(16px,5vw,32px) 0; }
.srch-filters summary { display:inline-flex; align-items:center; gap:8px; cursor:pointer; list-style:none; font-size:13.5px; font-weight:600; color:var(--ink-2); background:var(--card); border:1px solid var(--line-2); padding:9px 16px; border-radius:100px; }
.srch-filters summary::-webkit-details-marker { display:none; }
.srch-filters summary:hover { border-color:var(--acc); color:var(--acc-ink); }
.srch-filters summary .on { display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--acc); }
.srch-filters form { margin-top:12px; background:var(--card); border:1px solid var(--line); border-radius:18px; padding:18px; box-shadow:var(--sh-md); }
.srch-filters .grp { margin-bottom:16px; }
.srch-opts { display:flex; flex-wrap:wrap; gap:8px; }
.srch-opts input { position:absolute; opacity:0; pointer-events:none; }
.srch-opts label { font-size:13.5px; font-weight:500; color:var(--ink-2); background:var(--surf); border:1px solid var(--line-2); padding:8px 15px; border-radius:100px; cursor:pointer; transition:background .2s ease, border-color .2s ease, color .2s ease; }
.srch-opts label:hover { border-color:var(--acc); color:var(--acc-ink); }
.srch-opts input:checked + label { background:var(--btn); border-color:var(--btn); color:var(--on-acc); }
.srch-opts input:focus-visible + label { outline:2px solid var(--acc); outline-offset:2px; }
.srch-filters .acts { display:flex; align-items:center; justify-content:flex-end; gap:14px; padding-top:4px; }
.srch-filters .acts a { font-size:13.5px; font-weight:500; color:var(--mute); }
.srch-filters .acts a:hover { color:var(--acc); }
.srch-filters .acts button { border:none; cursor:pointer; font-family:inherit; font-size:14px; font-weight:600; color:var(--on-acc); background:var(--btn); padding:11px 22px; border-radius:100px; }
.srch-filters .acts button:hover { background:var(--btn-h); }

/* result cards */
.srch-best { display:block; background:var(--card); border:1px solid var(--line); border-radius:20px; overflow:hidden; box-shadow:var(--sh-md); transition:transform .25s ease, box-shadow .25s ease; }
.srch-best:hover { transform:translateY(-3px); box-shadow:var(--sh-xl); }
/* Height is set directly, NOT as aspect-ratio + max-height. That pair looks
   equivalent and is not: once max-height clamps the height, the width is
   re-derived from the ratio (300 x 16/9 = 533px in a 720px card), so the photo
   area shrank and exposed 186px of the card's background on the right.
   Measured, not guessed. The clamp keeps the 16/9 feel of design 08-results on
   narrow screens and stops the photo dominating a wide one. */
.srch-best .ph { position:relative; width:100%; height:clamp(190px,42vw,300px); background:var(--photo); display:flex; align-items:center; justify-content:center; }
.srch-best .ph img { width:100%; height:100%; object-fit:contain; }
.srch-best .ph .ph-none { width:44%; height:44%; object-fit:contain; opacity:.85; mix-blend-mode:multiply; }
.srch-pill { position:absolute; top:12px; right:12px; background:#FFF; color:var(--acc); font-size:12px; font-weight:700; letter-spacing:0.02em; padding:6px 13px; border-radius:100px; box-shadow:0 4px 12px rgba(70,50,35,0.12); }
.srch-best .body { padding:16px 18px 18px; }
.srch-best .nm { font-size:20px; font-weight:700; letter-spacing:-0.01em; margin:0 0 4px; color:var(--ink); }
.srch-best .sub { font-size:14px; color:var(--ink-2); margin:0; }
.srch-best .why, .srch-row .why { display:flex; gap:8px; margin:12px 0 0; font-size:13.5px; line-height:1.5; color:var(--ink-2); }
.srch-best .why svg, .srch-row .why svg { flex-shrink:0; margin-top:2px; }

.srch-row { display:flex; gap:12px; background:var(--card); border:1px solid var(--line); border-radius:18px; padding:12px; transition:border-color .2s ease, transform .2s ease, box-shadow .2s ease; }
.srch-row:hover { border-color:var(--acc-e); transform:translateY(-2px); box-shadow:var(--sh-md); }
.srch-row .th { flex-shrink:0; width:56px; height:70px; border-radius:12px; background:var(--photo); overflow:hidden; display:flex; align-items:center; justify-content:center; }
.srch-row .th img { width:100%; height:100%; object-fit:cover; }
.srch-row .th .ph-none { width:60%; height:60%; object-fit:contain; opacity:.85; mix-blend-mode:multiply; }
.srch-row .body { flex:1; min-width:0; }
.srch-row .top { display:flex; align-items:flex-start; gap:10px; }
.srch-row .nm { flex:1; min-width:0; font-size:15px; font-weight:600; color:var(--ink); margin:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.srch-row .pct { flex-shrink:0; font-size:12px; font-weight:700; color:var(--acc-ink); }
.srch-row .sub { font-size:12.5px; color:var(--mute); margin:2px 0 0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.srch-row .why { margin-top:6px; }
.srch-row .rt { display:flex; align-items:center; gap:5px; flex-shrink:0; font-size:13px; font-weight:600; color:var(--ink); }

/* Outbound retailer link on a list card.
   A card used to be one big <a>. It can't stay one: an anchor inside an anchor
   is invalid HTML and browsers silently unnest it, which would drop either the
   product link or the buy link. So the card is a positioned box, the product
   link stretches across it through ::after, and the buy pill sits above that
   overlay on z-index 2. */
.card, .srch-best, .srch-row { position:relative; }
.card-link { display:block; color:inherit; }
.card-link::after { content:""; position:absolute; inset:0; z-index:1; }
.buy-pill { position:relative; z-index:2; display:inline-flex; align-items:center; gap:6px; font-size:12.5px; font-weight:600; color:var(--acc-ink); background:var(--surf); border:1px solid var(--line-2); padding:7px 13px; border-radius:100px; white-space:nowrap; }
.buy-pill:hover { color:var(--acc); border-color:var(--acc); background:var(--card); }
.srch-best .buy-strip { padding:0 18px 18px; }
.srch-best .body { padding-bottom:12px; }
.srch-row .buy-slot { display:flex; align-items:center; flex-shrink:0; }
/* On a narrow screen the pill has nowhere to sit beside a 56px thumb and two
   lines of text, so it drops to its own line instead of squeezing the name. */
@media (max-width:520px) {
  .srch-row { flex-wrap:wrap; }
  .srch-row .buy-slot { width:100%; padding-top:4px; }
}

.srch-list { display:flex; flex-direction:column; gap:12px; margin-top:12px; }
.srch-note { font-size:14.5px; line-height:1.6; color:var(--ink-2); }
.srch-pager { display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:10px; margin-top:clamp(24px,4vw,36px); }
.srch-pager a { font-size:14px; font-weight:600; color:var(--acc-ink); border:1px solid var(--line-2); background:var(--card); padding:10px 18px; border-radius:100px; }
.srch-pager a:hover { border-color:var(--acc); }
.srch-pager span { font-size:14px; color:var(--mute); }
.srch-empty { text-align:center; background:var(--card); border:1px solid var(--line); border-radius:20px; padding:34px 24px; box-shadow:var(--sh-sm); }
.srch-empty h2 { font-size:20px; font-weight:600; margin:0 0 10px; }

/* ============================================================================
 * A box holding a product photograph.
 *
 * The photographs are cut out on white and that white is inside the JPEG, so
 * the box behind them cannot be darkened — the picture's own ground would show
 * as a rectangle within it. On a cream page that costs nothing. On a charcoal
 * one it leaves a bright rectangle per bottle, and eight of them in a grid read
 * as holes punched through the page rather than as pictures on it.
 *
 * Cutting the background out of the files was tried first and refused: the
 * flood-fill works on 29 of 32 bottles and fails on exactly the white and
 * silver ones, leaving ragged white shreds, with no reliable way to tell a
 * failure from a success without looking at it.
 *
 * So the plate stays and gets dressed instead. Three parts, none of which
 * touches the photograph's own pixels:
 *
 *   ring   a hairline INSIDE the plate, so it ends at a drawn edge
 *   film   an even veil of the page colour over plate AND photo together, which
 *          takes the glare off without opening a seam between the two — this is
 *          the part that could not be done by changing --photo
 *   lift   a shadow under the standalone plates, so they sit on the page
 *
 * All three are transparent/none in the light theme, so this whole block is a
 * no-op there rather than a second design to maintain.
 * ========================================================================== */
.shot, .dupe-shot, .claim-shot, .dupe-tile .ph, .srch-best .ph, .srch-row .th {
  position: relative;
}
/* Ring and film ride on the same overlay, and the ring is here rather than as
   an inset shadow on the box itself for a reason worth keeping: an inset shadow
   paints UNDER the element's content. A portrait bottle at object-fit:contain
   fills the box's full height, so it covered the ring along the top and bottom
   and left it showing only in the letterbox strips at the sides — measured,
   left and right edges had it, top and bottom did not. The overlay paints above
   the picture, so all four sides get the same edge. */
.shot::after, .dupe-shot::after, .claim-shot::after,
.dupe-tile .ph::after, .srch-best .ph::after, .srch-row .th::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit;
  background: var(--shot-film);
  box-shadow: inset 0 0 0 1px var(--shot-ring);
}
/* The lift goes only on plates that stand alone. On the plate that forms the
   top of a card the card already clips its overflow, so an outer shadow there
   would be drawn and then cut off at the same edge. */
.dupe-shot, .claim-shot, .pf-hero > .shot { box-shadow: var(--shot-lift); }
/* A badge sitting on a plate (the "Popular" pill, the match percentage) is
   inside the box, so the film covers it too. Left that way on purpose: the
   badge is printed on the picture, and 14% of the page colour over an already
   dark pill is not visible. What it must not do is beat these rules from a
   style attribute — an inline box-shadow on a plate silently drops the ring. */

/* ===== /fragrance/<slug> hero =====
 * The rest of that template styles itself inline; this block is here because a
 * style attribute cannot hold a media query. Everything below 720px is one
 * column: the image column is a fixed 340px track, so unlike the auto-fit grids
 * further down the page it never collapses by itself, and a phone was rendering
 * the desktop layout — a squeezed bottle beside accord chips two to a row. */
.pf-hero { display:grid; grid-template-columns:minmax(0,340px) 1fr; gap:clamp(20px,4vw,44px); align-items:start; }
@media (max-width:720px) {
  .pf-hero { grid-template-columns:1fr; }
  /* The photo box is square, so its width is also its height: left unbounded it
     would be 700px tall on a small tablet and push the name off the screen.
     420px is the point where it stops growing; a phone is narrower than that
     and still gets the full width. */
  .pf-hero > :first-child { max-width:420px; margin:0 auto; }
}

/* full-screen loader shown while a query is in flight (assets/search.js) */
.srch-veil { position:fixed; inset:0; z-index:200; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:18px; background:var(--veil); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); }
.srch-veil .sp { width:38px; height:38px; border:3px solid var(--line); border-top-color:var(--acc); border-radius:50%; animation:srch-spin .8s linear infinite; }
.srch-veil p { font-size:15px; color:var(--ink-2); margin:0; font-style:italic; }
@keyframes srch-spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce) { .srch-veil .sp { animation-duration:2s; } }

/* ===== scent chips: a note or accord with its picture (assets/notes.js) =====
 * Lives here rather than in style attributes because it needs :hover, :focus and
 * a hover-capability query, none of which a style attribute can hold — and
 * because this markup repeats ~17 times on each of 102k documents, so every byte
 * saved here is saved 17 times per page. */
.nchips { display:flex; flex-wrap:wrap; gap:7px; }

/* 44px is the floor for a thumb, and this is a mobile-first page. The chip is a
   button, so the browser's own font and background have to be reset. */
.nchip {
  display:inline-flex; align-items:center; gap:8px;
  min-height:44px; padding:4px 14px 4px 5px;
  font:inherit; font-size:13px; color:var(--ink-3);
  background:var(--card); border:1px solid var(--line-2); border-radius:100px;
  cursor:pointer; text-align:left;
}
/* A circle. It briefly became a rounded square, because 34% of the material
   photographs put their subject edge to edge and a round mask cut the corners
   off — but that is a fault in the pictures, and the pictures are ours to fix.
   The prompt now bounds the subject to half the cell width, and the cells that
   bled were regenerated. Squaring the frame would have been styling around a
   defect instead of removing it. */
.nchip img {
  width:34px; height:34px; border-radius:50%; object-fit:cover;
  background:var(--chip); flex:0 0 auto;
  /* A hairline, because the photographs are shot on cream linen: on the light
     theme the chip is cream too and the pale corners would dissolve into the
     page; on the dark one the edge instead keeps a bright disc from glowing
     straight into the surface around it. */
  box-shadow:inset 0 0 0 1px var(--ph-edge);
}
/* A note we have no picture for is the same chip with an empty tile, not a
   different kind of object: the flat beige pill it used to be stood out of the
   row far more than a missing picture deserves. Not a control though — there is
   nothing behind it to open. */
.nchip-flat { cursor:default; color:var(--ink-2); }
.nchip-flat .nthumb {
  width:34px; height:34px; border-radius:50%; flex:0 0 auto;
  background:var(--chip);
}
/* A chip with nothing behind it: no button, no promise — see seo_scent_chip().
   Every accord now carries a glossary entry, so in practice this is left for the
   case where one does not. The 44px floor stays even with no tap target to
   protect: a shorter chip in a row of taller ones reads as a different kind of
   thing rather than as the same thing without a card. */
.nchip-static { cursor:default; }
.nchip[aria-expanded="true"] { border-color:var(--acc); background:var(--surf); }
.nchip:focus-visible { outline:2px solid var(--acc); outline-offset:2px; }
/* Hover is the desktop garnish, never the mechanism — most of this traffic has
   no pointer at all, and on a touch screen :hover sticks after a tap. */
@media (hover:hover) and (pointer:fine) {
  .nchip:hover { border-color:var(--acc-e); }
}

/* The card. One per page, floating above the document rather than inserted into
   it: sitting in the flow, it pushed every row below it down as it opened and
   pulled them back up as it closed, so reading a note moved the thing you were
   reading. Absolute (not fixed) so it travels with the page when you scroll
   instead of hanging in the viewport. */
.ncard {
  display:flex; align-items:flex-start; gap:14px;
  position:absolute; z-index:60; top:0; left:0;
  /* 320px was sized for a note's one sentence. An accord's entry is a glossary
     definition and runs about three times that, which at 320 left a ~190px text
     column and a card thirteen lines tall on a phone. min() rather than a plain
     360 so the card still fits a 360px screen with its margins. */
  width:max-content; max-width:min(380px, calc(100vw - 32px)); padding:14px 38px 14px 14px;
  background:var(--surf); border:1px solid var(--line-2); border-radius:18px;
  box-shadow:var(--sh-xl), 0 2px 6px rgba(0,0,0,.08);
  opacity:0; transform:translateY(-4px);
  transition:opacity .16s ease, transform .16s ease;
  pointer-events:none;
}
.ncard.is-open { opacity:1; transform:translateY(0); pointer-events:auto; }
.ncard[hidden] { display:none; }
/* Someone who asked not to be moved is not asking for a slower move. */
@media (prefers-reduced-motion:reduce) {
  .ncard { transition:none; transform:none; }
}
.ncard img {
  /* Rounded rectangle here, circle on the chip. The chip is 34px and reads as
     an icon, where a circle is the friendlier shape; the card is 96px and is
     showing you the photograph, so it keeps the whole frame the model composed
     instead of cropping the corners off it. */
  width:96px; height:96px; border-radius:12px; object-fit:cover;
  background:var(--chip); flex:0 0 auto;
  box-shadow:inset 0 0 0 1px var(--ph-edge);
}
.ncard h3 { font-size:15px; font-weight:600; margin:2px 0 6px; color:var(--ink); }
.ncard p { font-size:14px; line-height:1.55; color:var(--ink-2); margin:0; }
.ncard button {
  position:absolute; top:8px; right:8px; width:30px; height:30px;
  font:inherit; font-size:18px; line-height:1; color:var(--mute);
  background:none; border:0; border-radius:50%; cursor:pointer;
}
.ncard button:hover { background:var(--chip); }

/* ============================================================================
 * /dupes/{original} — what clone houses say they copied.
 *
 * Here rather than in style attributes because all of it is either a media
 * query or a grid whose placement changes with width, and a style attribute can
 * hold neither.
 * ========================================================================== */

.dupe-hero { display:grid; grid-template-columns:210px 1fr; gap:clamp(20px,4vw,40px); align-items:start;
  padding-bottom:clamp(26px,4vw,40px); border-bottom:1px solid var(--line); }
/* White, and contained rather than covered: the catalogue's shots are not one
   shape (this one is 3:4), so covering cropped the cap off the bottle. The
   photographs carry their own white background, so any other ground would show
   as a border around the product — which is also why this box does not follow
   the theme. Same reasoning as the perfume hero. */
.dupe-shot { background:var(--photo); border:1px solid var(--line); border-radius:22px; aspect-ratio:1/1;
  overflow:hidden; }
.dupe-shot img { width:100%; height:100%; object-fit:contain; }

.dupe-facts { display:flex; flex-wrap:wrap; gap:10px 18px; margin:16px 0 0; padding:0; list-style:none;
  font-size:14px; color:var(--ink-2); font-variant-numeric:tabular-nums; }
.dupe-facts b { color:var(--ink); font-weight:600; }

/* Caption above its chips, never beside them. Beside worked only while a layer
   was short enough to fit one line, so TOP kept its label on the left while BASE
   wrapped and pushed it onto its own row — the same list reading as two layouts
   down one column. */
.dupe-pyramid { display:flex; flex-direction:column; gap:16px; margin-top:22px; }
.dupe-layer { display:flex; flex-direction:column; align-items:flex-start; gap:8px; }
.dupe-layer .lbl { font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.09em;
  color:var(--mute); }

/* The claim. Bottle on the left spanning the card; house and product name beside
   it; the quote and its provenance below, across both columns on a phone. */
.claim { background:var(--card); border:1px solid var(--line); border-radius:20px;
  box-shadow:var(--sh-sm); padding:18px clamp(16px,2.5vw,24px); margin-bottom:12px;
  display:grid; grid-template-columns:104px 1fr; column-gap:clamp(14px,2vw,22px); row-gap:12px;
  align-items:start; }
.claim-noshot { grid-template-columns:1fr; }
.claim-shot { background:var(--photo); border:1px solid var(--line); border-radius:16px; aspect-ratio:1/1;
  overflow:hidden; grid-row:1 / span 3; }
.claim-shot img { width:100%; height:100%; object-fit:contain; }
/* House above the name, always. Side by side they stayed on one line only while
   both were short, so "DUA" + a long title wrapped and "ALEXANDRIA" + a short
   one did not. */
.claim-top { grid-column:2; display:flex; flex-direction:column; align-items:flex-start; gap:7px; }
.claim-noshot .claim-top, .claim-noshot blockquote, .claim-noshot .stamp { grid-column:1; }
.claim-house { align-self:flex-start; background:var(--acc); color:var(--on-acc); border-radius:100px;
  padding:3px 11px; font-size:11px; font-weight:600; letter-spacing:.07em; text-transform:uppercase; }
.claim-name { font-size:19px; font-weight:600; letter-spacing:-.015em; }
.claim blockquote { grid-column:2; margin:0; padding:0 0 0 16px; border-left:2px solid var(--acc);
  font-size:16.5px; line-height:1.55; color:var(--ink); }
.claim blockquote .said { display:block; margin-bottom:5px; font-size:14.5px; color:var(--ink-2); }
/* The provenance stamp. The page's whole claim to trust is that these sentences
   are quoted rather than concluded, so the chain of custody is printed. */
.claim .stamp { grid-column:2; display:flex; flex-wrap:wrap; gap:8px 16px; padding-top:12px;
  border-top:1px dashed var(--line-2); font-size:12.5px; color:var(--mute); font-variant-numeric:tabular-nums; }
.claim .stamp b { font-weight:500; color:var(--ink-2); }

/* Our own data sits on its own ground, so a skim cannot take it for a claim. */
.dupe-ours { background:var(--surf); border:1px solid var(--line); border-radius:24px;
  padding:clamp(22px,3vw,32px); margin-top:clamp(34px,5vw,54px); }

@media (max-width:640px) {
  /* The bottle bleeds to the page edge by cancelling the wrapper's own inline
     padding. Not 100vw + translate: that leaves the original box in place and
     the leftover shows up as a horizontal scrollbar. */
  .dupe-hero { grid-template-columns:1fr; gap:20px; }
  .dupe-shot { width:auto; margin-inline:calc(-1 * clamp(20px,5vw,48px)); aspect-ratio:4/3;
    border-radius:0; border-left:0; border-right:0; }
}
@media (max-width:520px) {
  /* The bottle keeps the house and the name company on its row; only the quote
     drops below. Stacking the photo alone left a phone-width band of nothing
     beside it. */
  .claim { grid-template-columns:84px 1fr; row-gap:10px; }
  .claim-shot { grid-row:1; }
  .claim-top { align-self:center; }
  .claim blockquote, .claim .stamp { grid-column:1 / -1; }
}

/* /dupes — the index. Tiles, not the catalogue's product cards: there is no
 * rating and no buy link here, because both would read as a verdict on copies
 * we have not smelled. The one number is how many houses have said it. */
.dupe-index { display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
  gap:clamp(12px,1.6vw,18px); }
.dupe-tile { display:flex; flex-direction:column; background:var(--card); border:1px solid var(--line);
  border-radius:18px; overflow:hidden; box-shadow:var(--sh-sm); color:inherit; }
.dupe-tile .ph { aspect-ratio:1/1; background:var(--photo); border-bottom:1px solid var(--hair);
  display:flex; align-items:center; justify-content:center; }
/* Contained, like every other bottle on the site: the catalogue's shots are not
   one shape and covering crops the cap off the tall ones. */
.dupe-tile .ph img { width:100%; height:100%; object-fit:contain; }
.dupe-tile .ph img.glyph { width:46%; height:46%; opacity:.9; mix-blend-mode:multiply; }
.dupe-tile .body { padding:12px 14px 14px; display:flex; flex-direction:column; gap:4px; flex:1; }
.dupe-tile .br { font-size:10.5px; font-weight:600; text-transform:uppercase; letter-spacing:.08em;
  color:var(--mute); }
.dupe-tile h2 { font-size:15px; font-weight:600; margin:0; letter-spacing:-.01em; line-height:1.22;
  color:var(--ink); }
.dupe-tile .n { margin-top:auto; padding-top:8px; font-size:12.5px; font-weight:600; color:var(--acc-ink);
  font-variant-numeric:tabular-nums; }
@media (hover:hover) and (pointer:fine) {
  .dupe-tile { transition:transform .18s ease, box-shadow .18s ease, border-color .18s ease; }
  .dupe-tile:hover { transform:translateY(-2px); border-color:var(--acc-e);
    box-shadow:var(--sh-xl); }
}
@media (prefers-reduced-motion:reduce) { .dupe-tile { transition:none; } }
.dupe-tile:focus-visible { outline:2px solid var(--acc); outline-offset:2px; }

/* The /dupes header band. Full width because the page's problem was the top:
 * one column of text against a lot of cream at 1 180px in a 2 000px viewport.
 * A texture behind the grid would have been the wrong answer — the bottles are
 * the page, and anything behind them competes with 90 product shots. */
.dupe-band { background:var(--surf); border-bottom:1px solid var(--line); }
.dupe-band .in { max-width:1180px; margin:0 auto;
  padding:clamp(20px,3.5vw,38px) clamp(20px,5vw,48px) clamp(24px,3.5vw,40px);
  display:grid; grid-template-columns:1fr auto; gap:clamp(24px,4vw,56px); align-items:end; }
/* The tally, so the right half carries information rather than air. */
.dupe-tally { background:var(--card); border:1px solid var(--line); border-radius:20px;
  padding:18px 22px; box-shadow:var(--sh-sm); min-width:236px; }
.dupe-tally .rows { display:flex; gap:26px; padding-bottom:14px; margin-bottom:12px;
  border-bottom:1px solid var(--hair); }
.dupe-tally .big { display:flex; flex-direction:column; }
.dupe-tally .n { font-size:26px; font-weight:600; letter-spacing:-.02em; color:var(--ink);
  font-variant-numeric:tabular-nums; line-height:1.1; }
.dupe-tally .l { font-size:12px; color:var(--mute); }
.dupe-tally ul { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:6px; }
.dupe-tally li { display:flex; justify-content:space-between; gap:18px; font-size:13.5px;
  color:var(--ink-2); font-variant-numeric:tabular-nums; }
.dupe-tally li b { color:var(--ink); font-weight:600; }
@media (max-width:760px) {
  /* Stacked, and the tally goes first in reading order only on wide screens —
     on a phone the sentence has to come before the numbers it explains. */
  .dupe-band .in { grid-template-columns:1fr; gap:22px; align-items:start; }
  .dupe-tally { min-width:0; }
}
