/* PROTOTYPE — "mystique" luminous animated background theme.
   Applied to exactly ONE article (heart-line-meaning.html) as a demo for
   Cezary's approval before any wider rollout. Independent of
   article-papyrus.css / article.css — neither of those files is touched.

   IMPORTANT: no Gemini API key was reachable from this local dev
   environment (checked: no repo .env, no env var, GitHub/Cloud Run only
   expose env var *names* not values). So this background is CSS-gradient
   only — no AI-generated image asset was produced or faked. Palette is
   deliberately LIGHT / vibrant / luminous (opal, radiant gold, aurora
   teal, lavender, blush) — the opposite of the dark near-black
   deep-magenta jewel-tone palette used by publisher/imagegen.py for
   Facebook post images, which this file does not touch or reuse.

   Animation: a slow Ken-Burns zoom/pan + hue drift on the aurora scene
   layer, a drifting/twinkling sparkle-particle layer, and a slow pulsing
   glow wash — all pure CSS transform/opacity/filter/background-position,
   fixed-position full-bleed pseudo-elements, no JS, no canvas. Respects
   prefers-reduced-motion. */

:root{
  --ink:#0d0a08;          /* primary text — near-black, per Cezary's request */
  --ink-soft:#221a14;     /* secondary text — soft near-black */
  --card:#fbf6ff;         /* near-solid pale lavender-white card surface */
  --card-strong:#ffffff;
  --line:#e4d3ff;
  --gold:#f5a623;
  --gold-deep:#d9860f;
  --teal:#29c7b3;
  --lavender:#b48cff;
  --pink:#ff8fd0;
}

*{box-sizing:border-box}

html,body{height:100%}

/* 2026-08-19 fix: body's own background was a flat opaque near-white
   (#fdf9ff). Because html has no background of its own, body's declared
   background propagates to become the actual page CANVAS background --
   which paints behind EVERYTHING, including the animated html::before /
   html::after / body::before layers (they have negative z-index but still
   sit above the canvas). Those three layers are `position:fixed`, so they
   only ever cover the CURRENT viewport -- which is exactly what a real
   scrolling user sees at every scroll position (verified: they always
   fill the visible viewport as you scroll, the animated background truly
   is continuous for a real reader). But `position:fixed` elements are a
   known gap for full-page/"capture beyond viewport" screenshot tooling
   (Chromium DevTools "capture full-size screenshot", Playwright/Puppeteer
   fullPage, and by extension things like Lighthouse/PSI renders) -- those
   tools render fixed elements pinned to the ORIGINAL viewport slice only,
   not repeated down a tall stitched capture, so the canvas color used to
   show through as a flat white band for the rest of the document in any
   such capture. Two changes fix this without touching the real-user
   experience (which was already correct):
   1. background-color is now a dark, mystique-palette tone instead of
      near-white, so any canvas gap blends with the theme instead of
      flashing to a stray white page.
   2. body also carries the SAME per-article --mystique-bg image, tiled
      down the full canvas (background-repeat:repeat-y), so the canvas
      layer itself is colorful/on-theme for its entire height -- visible
      only where the fixed layers don't reach (screenshot-capture
      artifacts, or a not-yet-painted frame during fast mobile momentum
      scroll), never in normal interactive scrolling. */
body{
  margin:0;
  background-color:#150c22;
  background-image: var(--mystique-bg, none);
  background-repeat: repeat-y;
  background-size: 100% auto;
  background-position: center top;
  color:var(--ink);
  font:18px/1.8 'Inter',-apple-system,Segoe UI,Roboto,sans-serif;
  position:relative;
  isolation:isolate; /* new stacking context so the fixed bg layers on html don't fight body content */
}

/* ---------- Layer A: real AI-generated mystic scene (per-article, set via
   --mystique-bg on :root in the article's own <head>) — slow Ken-Burns
   zoom/pan + gentle hue drift for cinematic motion. background-size:cover
   on a fixed pseudo-element (not an <img>), so the max-width:100% img
   reset that clamps JS/CSS-scaled <img> transforms elsewhere never applies
   here — deliberate, not an oversight. ---------- */
html::before{
  content:"";
  position:fixed;
  inset:-8%;
  z-index:-3;
  pointer-events:none;
  background-image:
    radial-gradient(ellipse 70% 55% at 50% 40%, rgba(255,255,255,0.14) 0%, rgba(255,255,255,0) 60%),
    var(--mystique-bg, none);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  animation: mystiqueKenBurns 14s ease-in-out infinite, mystiqueHue 9s ease-in-out infinite;
  will-change: transform, filter;
}

@keyframes mystiqueKenBurns{
  0%   { transform: scale(1.05) translate(0%, 0%); }
  50%  { transform: scale(1.30) translate(-4.5%, 3%); }
  100% { transform: scale(1.05) translate(0%, 0%); }
}

/* On tall/narrow (mobile) viewports, background-size:cover on this
   landscape scene forces a huge scale-up to fill the height, cropping the
   image down to little more than the centered palm and losing the rest of
   the composition (zodiac wheel, wings) — reported by Cezary as "the hand
   is far too big, I want to see the full palm". Switching to plain
   background-size:contain fixed that but left blank white bars above/below
   the image — reported next as "remove white areas completely". Fix: two
   layers instead of one. html::before keeps cover (still fills edge-to-
   edge, no white space) but becomes a heavily blurred, dimmed ambient fill
   — a blurred backdrop, not the readable image, so its cropping doesn't
   matter. main::before sits on top at exact inset:0 with background-size:
   contain, so the WHOLE uncropped scene is always visible centered over
   that blurred fill, with no gaps. Motion stays "slow" per Cezary's ask:
   the blurred fill keeps a slowed-down Ken-Burns pan (26s, was 14s), and
   the crisp foreground layer gets its own slow hue/brightness drift
   instead of scaling — recoloring it doesn't need to crop, so the full
   palm never gets zoomed back out of frame. */
@media (max-width: 700px), (orientation: portrait){
  html::before{
    filter: blur(28px) brightness(0.9) saturate(1.1);
    /* base scale lives in the keyframe (starts at 1.20) so the blurred fill
       is already padded well past the blur radius before it even starts
       animating -- no separate static transform needed. */
    animation: mystiqueKenBurnsSlow 26s ease-in-out infinite, mystiqueHue 18s ease-in-out infinite;
  }
  main{position:relative}
  main::before{
    content:"";
    position:fixed;
    inset:0;
    z-index:-1;
    pointer-events:none;
    background-image: var(--mystique-bg, none);
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    /* Feather the crisp layer's edges so it dissolves into the blurred fill
       behind it instead of reading as a hard-edged rectangle pasted on top.
       background-size:contain on this landscape image inside a tall
       portrait box is width-limited, so the image itself only occupies a
       horizontal band roughly in the vertical middle third of the box (the
       hard seam is top/bottom, not left/right, which already span the full
       box width) -- a vertical gradient targeted at that band, not a
       centered ellipse over the whole box, is what actually reaches the
       image's real edges. */
    /* 2026-08-19 fix: the previous stops (transparent 24% -> black 40%,
       black 58% -> transparent 74%) were a guess that didn't match where
       a 16:9 generated image (the fixed aspect ratio publisher/imagegen.py
       always requests) actually lands under background-size:contain in a
       real portrait mobile box. Worked out from geometry across common
       phone widths (iPhone SE 375x667 through Pro Max 430x932, plus
       412x915 Android): the real image band's top edge falls at ~34%-37.5%
       of box height and its bottom edge at ~62.5%-66%, essentially
       constant regardless of viewport width because both box and image
       scale together. The old stops started ramping in at 24% and didn't
       reach full opacity till 40% -- i.e. part of that ramp (24%-34%) was
       fading in over blank letterboxed space (harmless) but the tail
       (34%-40%) was fading in RIGHT ON TOP of real image content, showing
       it part-transparent over a differently-cropped/scaled blur layer of
       the same source photo -- a visible hard-edged seam where the two
       mismatched crops overlap. New stops keep transparent right up to
       28% (just outside the earliest real edge across devices), ease in
       through an intermediate low-alpha stop at 34% (dead center of the
       real-edge range) to fully opaque by 40% -- and mirror on the way
       out -- so the blend zone straddles the actual image edge on every
       common phone width instead of guessing past it. */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, transparent 28%, rgba(0,0,0,0.28) 34%, black 40%, black 58%, rgba(0,0,0,0.28) 64%, transparent 70%, transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0%, transparent 28%, rgba(0,0,0,0.28) 34%, black 40%, black 58%, rgba(0,0,0,0.28) 64%, transparent 70%, transparent 100%);
    animation: mystiqueGlowSlow 18s ease-in-out infinite;
  }
}
@keyframes mystiqueKenBurnsSlow{
  0%   { transform: scale(1.20) translate(0%, 0%); }
  50%  { transform: scale(1.32) translate(-3%, 2%); }
  100% { transform: scale(1.20) translate(0%, 0%); }
}
@keyframes mystiqueGlowSlow{
  0%   { filter: hue-rotate(0deg) saturate(1) brightness(1); }
  50%  { filter: hue-rotate(14deg) saturate(1.15) brightness(1.05); }
  100% { filter: hue-rotate(0deg) saturate(1) brightness(1); }
}
@keyframes mystiqueHue{
  0%   { filter: hue-rotate(0deg) saturate(1) brightness(1); }
  50%  { filter: hue-rotate(22deg) saturate(1.35) brightness(1.08); }
  100% { filter: hue-rotate(0deg) saturate(1) brightness(1); }
}

/* ---------- Layer B: drifting / twinkling sparkle particles ---------- */
html::after{
  content:"";
  position:fixed;
  inset:0;
  z-index:-2;
  pointer-events:none;
  background-repeat:no-repeat;
  background-image:
    radial-gradient(circle, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 70%),
    radial-gradient(circle, rgba(245,166,35,0.9) 0%, rgba(245,166,35,0) 70%),
    radial-gradient(circle, rgba(41,199,179,0.9) 0%, rgba(41,199,179,0) 70%),
    radial-gradient(circle, rgba(255,143,208,0.9) 0%, rgba(255,143,208,0) 70%),
    radial-gradient(circle, rgba(180,140,255,0.9) 0%, rgba(180,140,255,0) 70%),
    radial-gradient(circle, rgba(255,255,255,0.85) 0%, rgba(255,255,255,0) 70%),
    radial-gradient(circle, rgba(245,166,35,0.8) 0%, rgba(245,166,35,0) 70%),
    radial-gradient(circle, rgba(41,199,179,0.8) 0%, rgba(41,199,179,0) 70%),
    radial-gradient(circle, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0) 70%),
    radial-gradient(circle, rgba(180,140,255,0.85) 0%, rgba(180,140,255,0) 70%),
    radial-gradient(circle, rgba(255,143,208,0.8) 0%, rgba(255,143,208,0) 70%),
    radial-gradient(circle, rgba(245,166,35,0.75) 0%, rgba(245,166,35,0) 70%);
  background-size:
    10px 10px, 14px 14px, 8px 8px, 12px 12px, 9px 9px, 7px 7px,
    16px 16px, 11px 11px, 6px 6px, 13px 13px, 9px 9px, 10px 10px;
  background-position:
    8% 15%, 22% 62%, 38% 8%, 52% 40%, 66% 78%, 78% 22%,
    88% 55%, 12% 85%, 45% 90%, 92% 12%, 30% 30%, 60% 65%;
  animation: sparkleDrift 20s linear infinite, sparkleTwinkle 2.6s ease-in-out infinite alternate;
  will-change: background-position, opacity;
}

@keyframes sparkleDrift{
  0%{
    background-position:
      8% 15%, 22% 62%, 38% 8%, 52% 40%, 66% 78%, 78% 22%,
      88% 55%, 12% 85%, 45% 90%, 92% 12%, 30% 30%, 60% 65%;
  }
  100%{
    background-position:
      11% 22%, 18% 55%, 43% 14%, 47% 47%, 61% 84%, 82% 16%,
      84% 61%, 16% 79%, 40% 96%, 88% 18%, 34% 24%, 56% 71%;
  }
}
@keyframes sparkleTwinkle{
  0%{opacity:0.25}
  100%{opacity:1}
}

/* ---------- Layer C: slow pulsing aura wash ---------- */
body::before{
  content:"";
  position:fixed;
  inset:0;
  z-index:-1;
  pointer-events:none;
  background: radial-gradient(ellipse 80% 60% at 50% 30%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 70%);
  animation: auraPulse 4.5s ease-in-out infinite alternate;
  will-change: opacity;
}
@keyframes auraPulse{
  0%{opacity:0.2}
  100%{opacity:0.8}
}

/* ---------- Accessibility: respect prefers-reduced-motion ---------- */
@media (prefers-reduced-motion: reduce){
  html::before, html::after, body::before, main::before{
    animation: none !important;
  }
}

a{color:var(--gold-deep);font-weight:600;text-decoration-color:rgba(217,134,15,0.55)}
a:hover{text-decoration:underline}

.nav{
  display:flex;align-items:center;justify-content:space-between;
  flex-wrap:wrap;row-gap:10px;
  max-width:860px;margin:0 auto;padding:14px 22px;
  position:relative;z-index:1;
}
.nav .brand{
  display:flex;align-items:center;gap:9px;
  font-family:'Cinzel',serif;font-weight:700;font-size:19px;
  /* Fixed dark ink, independent of --ink — this chip has its own solid-ish
     white backdrop regardless of what the body text color is set to, so it
     must never inherit --ink (white-on-white went invisible on mobile). */
  color:#241536;
  background:rgba(255,255,255,0.6);
  padding:6px 12px;border-radius:20px;
  backdrop-filter: blur(6px);
}
.nav .star{width:26px;height:26px}
.cta{
  background:linear-gradient(135deg,var(--gold),var(--pink));
  color:#2a1300;font-weight:700;padding:9px 18px;border-radius:30px;font-size:14px;
  box-shadow:0 4px 14px rgba(245,166,35,0.35);
  white-space:nowrap;
}
.cta:hover{text-decoration:none;filter:brightness(1.06)}

/* No card/box behind the text — it sits directly on the animated
   background. Legibility comes from a soft dark text-shadow halo on every
   text element instead (see below), not from a solid/near-solid panel. */
main{
  position:relative;
  z-index:1;
  max-width:760px;margin:20px auto 70px;padding:0;
}
main > *{position:relative;z-index:1;padding-left:26px;padding-right:26px}
main > h1:first-child, main > .crumb:first-child{padding-top:22px}

/* No card behind any of this — text sits straight on the animated photo.
   Black text (per Cezary's request) needs a LIGHT halo, not a dark one —
   black-on-the-magenta/purple-hand-region would vanish with a dark halo;
   a white/light halo lifts it off any patch of the busy background. */
.crumb{color:var(--ink-soft);font-size:13px;margin:14px 0 6px;font-weight:700;text-shadow:0 1px 3px #fff, 0 0 10px rgba(255,255,255,0.9), 0 0 22px rgba(255,255,255,0.65)}
h1{font-family:'Cinzel',serif;font-size:33px;line-height:1.25;margin:8px 0 6px;color:var(--ink);font-weight:700;text-shadow:0 1px 4px #fff, 0 0 14px rgba(255,255,255,0.95), 0 0 30px rgba(255,255,255,0.7)}
.lede{color:var(--ink-soft);font-size:19px;margin:0 0 24px;font-weight:700;text-shadow:0 1px 3px #fff, 0 0 12px rgba(255,255,255,0.9), 0 0 26px rgba(255,255,255,0.65)}
h2{font-family:'Cinzel',serif;color:var(--ink);font-size:24px;margin:36px 0 8px;font-weight:700;text-shadow:0 1px 4px #fff, 0 0 14px rgba(255,255,255,0.95), 0 0 30px rgba(255,255,255,0.7)}
h3{color:var(--ink);font-size:19px;margin:26px 0 6px;font-family:'Cinzel',serif;font-weight:700;text-shadow:0 1px 3px #fff, 0 0 12px rgba(255,255,255,0.9), 0 0 26px rgba(255,255,255,0.65)}
p,li{color:var(--ink);font-weight:600;text-shadow:0 1px 3px #fff, 0 0 10px rgba(255,255,255,0.9), 0 0 22px rgba(255,255,255,0.65)}
blockquote{border-left:3px solid var(--gold-deep);margin:18px 0;padding:6px 18px;background:rgba(245,166,35,0.08);color:var(--ink-soft);font-style:italic;font-weight:500}
.tip{background:rgba(41,199,179,0.08);border:2px solid var(--teal);border-radius:12px;padding:16px 20px;margin:22px 0}
img.hero{width:100%;border-radius:14px;border:1px solid var(--line);margin:14px 0}
.appbox{
  background:linear-gradient(135deg, rgba(180,140,255,0.10), rgba(245,166,35,0.10));
  border:2px solid var(--line);border-radius:16px;padding:24px;margin:34px 0;text-align:center;
}
.appbox h3{margin:0 0 8px;color:var(--ink)}
.appbox p{color:var(--ink-soft) !important;font-weight:500}
.appbox a.cta{display:inline-block;margin-top:6px}
.related{border-top:2px solid rgba(180,140,255,0.25);margin-top:40px;padding-top:18px;padding-bottom:26px}
.related strong{color:var(--ink);font-family:'Inter',sans-serif;font-size:13px;text-transform:uppercase;letter-spacing:0.06em;font-weight:700}
.related a{display:block;padding:8px 0;border-bottom:1px solid rgba(180,140,255,0.15)}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px;margin-top:18px}
.card{background:var(--card-strong);border:1px solid var(--line);border-radius:14px;padding:20px;transition:.2s}
.card:hover{border-color:var(--gold);transform:translateY(-2px)}
/* .card and footer keep their own fixed dark ink — both sit on solid-ish
   light backgrounds of their own regardless of the main --ink choice
   (white text went invisible here the same way the nav chip did). */
.card h3{margin:0 0 6px;color:#241536}.card p{color:#3a2456;font-size:15px;margin:0}

footer{
  position:relative;z-index:1;
  border-top:none;text-align:center;color:#3a2456;font-size:13px;padding:20px;
  background:rgba(255,255,255,0.55);
  backdrop-filter: blur(6px);
  margin-top:10px;
}
footer a{color:var(--gold-deep)}
.star{display:inline-block}
