/* ==========================================================================
   The pattern layer.

   Split out of site.css so the storefront and the admin contact sheet render
   the mandala from ONE source. The contact sheet exists to decide how loud the
   pattern should be; a contact sheet drawing from a second copy of these rules
   would be deciding about something other than the site.

   Imported by tokens.css, which both site.css and admin.css already pull in.
   ========================================================================== */
/* ==========================================================================
   The mandala layer

   Two counter-rotating mandalas per bloom, sized well past their container and
   anchored off an edge so the composition always runs out of frame. The
   container clips, but the drawing is large enough that no ring ever closes
   inside the viewport, which is what stops it reading as a boxed panel.
   ========================================================================== */
.rel { position: relative; isolation: isolate; }
.clip { overflow: hidden; }

.bloom { position: absolute; inset: 0; z-index: -1; pointer-events: none; overflow: visible; }
.bloom span {
  position: absolute; display: block;
  width: var(--bloom-size); height: var(--bloom-size);
  animation: spin var(--spin, 300s) linear infinite;
  will-change: transform;
}
.bloom svg { width: 100%; height: 100%; display: block; }
.bloom__b { animation-direction: reverse; }

/* anchors put the centre outside the box so only an arc of the ring is seen */
.bloom--right  .bloom__a { top: 50%; right: -34%; transform: translateY(-50%); }
.bloom--right  .bloom__b { top: 8%;  right: -12%; width: calc(var(--bloom-size) * .62); height: calc(var(--bloom-size) * .62); }
.bloom--left   .bloom__a { top: 50%; left: -34%;  transform: translateY(-50%); }
.bloom--left   .bloom__b { bottom: -18%; left: -8%; width: calc(var(--bloom-size) * .58); height: calc(var(--bloom-size) * .58); }
.bloom--top    .bloom__a { left: 50%; top: -62%;  transform: translateX(-50%); }
.bloom--top    .bloom__b { right: -14%; bottom: -30%; width: calc(var(--bloom-size) * .55); height: calc(var(--bloom-size) * .55); }
.bloom--bottom .bloom__a { left: 50%; bottom: -66%; transform: translateX(-50%); }
.bloom--bottom .bloom__b { left: -16%; top: -24%; width: calc(var(--bloom-size) * .6); height: calc(var(--bloom-size) * .6); }
.bloom--centre .bloom__a { left: 50%; top: 50%; transform: translate(-50%, -50%); }
.bloom--centre .bloom__b { left: 50%; top: 50%; transform: translate(-50%, -50%); width: calc(var(--bloom-size) * .52); height: calc(var(--bloom-size) * .52); }

/* A tone used to set one `color` for the whole mandala, so every ring, petal,
   dot and fringe in it came out the same ink. Against a dense polychrome
   reference that reads as a faint smudge no matter how good the geometry is.
   A tone now sets four inks: the three bands and a counterpoint. They are drawn
   from the block-print accents of sibling palettes, so a colourway stays in
   family with whichever of the 24 palettes is active rather than importing
   colours from nowhere. */
.tone-gold {
  color: var(--accent);
  --pat-c1: var(--accent); --pat-c2: var(--pat-1); --pat-c3: var(--pat-4); --pat-c4: var(--pat-2);
  opacity: calc(.10 * var(--pattern-intensity, 1));
}
.tone-madder {
  color: var(--pat-1);
  --pat-c1: var(--pat-1); --pat-c2: var(--pat-4); --pat-c3: var(--accent); --pat-c4: var(--pat-5);
  opacity: calc(.075 * var(--pattern-intensity, 1));
}
.tone-indigo {
  color: var(--pat-2);
  --pat-c1: var(--pat-2); --pat-c2: var(--pat-3); --pat-c3: var(--accent); --pat-c4: var(--pat-1);
  opacity: calc(.07 * var(--pattern-intensity, 1));
}
.tone-viridian {
  color: var(--pat-3);
  --pat-c1: var(--pat-3); --pat-c2: var(--pat-2); --pat-c3: var(--pat-4); --pat-c4: var(--accent);
  opacity: calc(.07 * var(--pattern-intensity, 1));
}
.tone-kesar {
  color: var(--pat-4);
  --pat-c1: var(--pat-4); --pat-c2: var(--accent); --pat-c3: var(--pat-1); --pat-c4: var(--pat-3);
  opacity: calc(.085 * var(--pattern-intensity, 1));
}
.tone-jamun {
  color: var(--pat-5);
  --pat-c1: var(--pat-5); --pat-c2: var(--pat-2); --pat-c3: var(--accent); --pat-c4: var(--pat-4);
  opacity: calc(.07 * var(--pattern-intensity, 1));
}

/* Loud where nothing sits on top, hush where body copy does. A mandala that
   competes with a headline has stopped being elegant; a mandala nobody can see
   was not worth drawing. The intensity multiplier moves all of it at once and
   is set from Admin, Settings, so it is tunable without a deploy. */
.bloom--loud span            { opacity: calc(.15 * var(--pattern-intensity, 1)); }
.bloom--loud .tone-gold      { opacity: calc(.19 * var(--pattern-intensity, 1)); }
.bloom--hush span            { opacity: calc(.05 * var(--pattern-intensity, 1)); }

/* Fade the field where the copy sits, so the mandala is densest out at the
   edges it runs off and quietest under the words. */
.bloom span {
  -webkit-mask-image: radial-gradient(closest-side, rgba(0,0,0,.15) 0%, rgba(0,0,0,.55) 42%, #000 78%);
          mask-image: radial-gradient(closest-side, rgba(0,0,0,.15) 0%, rgba(0,0,0,.55) 42%, #000 78%);
}

.psych .bloom__a { animation: spin var(--spin, 300s) linear infinite, hue 46s ease-in-out infinite; }
.psych .bloom__b { animation: spin var(--spin, 300s) linear infinite reverse, hue 62s ease-in-out infinite -12s; }

@keyframes spin { to { transform: rotate(360deg); } }
@keyframes hue {
  0%, 100% { filter: hue-rotate(0deg)   saturate(1); }
  25%      { filter: hue-rotate(34deg)  saturate(1.5); }
  50%      { filter: hue-rotate(-26deg) saturate(1.25); }
  75%      { filter: hue-rotate(48deg)  saturate(1.6); }
}
/* anchored blooms keep their translate, so spin has to compose with it */
.bloom--right .bloom__a, .bloom--left .bloom__a { animation-name: spinY; }
.bloom--top .bloom__a, .bloom--bottom .bloom__a { animation-name: spinX; }
.bloom--centre .bloom__a, .bloom--centre .bloom__b { animation-name: spinC; }
.psych .bloom--centre .bloom__a { animation-name: spinC, hue; }
@keyframes spinY { from { transform: translateY(-50%) rotate(0); }      to { transform: translateY(-50%) rotate(360deg); } }
@keyframes spinX { from { transform: translateX(-50%) rotate(0); }      to { transform: translateX(-50%) rotate(360deg); } }
@keyframes spinC { from { transform: translate(-50%,-50%) rotate(0); }  to { transform: translate(-50%,-50%) rotate(360deg); } }

/* small tiled field, only where a mandala has no room */
.pat { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }
.pat--ambient { opacity: .06; }
.pat--wave { opacity: .16; animation: slide 44s linear infinite; }
@keyframes slide { to { transform: translate3d(-240px, 0, 0); } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .bloom span, .pat, .psych .bloom__a, .psych .bloom__b {
    animation: none !important; filter: none !important;
  }
  * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
}

/* -- motion: how the mandala behaves ---------------------------------------

   Scoped on [data-motion] rather than :root[data-motion]. The attribute still
   lives on <html> in normal use, but matching any element means a single tile
   can preview a motion on its own, which is what the admin contact sheet needs
   in order to be a contact sheet rather than nine identical pictures.

   The bands used to be three fixed groups (.mz--in, .mz--mid, .mz--out) and
   the motion was rotation plus a faint opacity pulse. The composition is
   per-motif now and emits three to five bands as .mz--b0 upward, each carrying
   its own --band index, so the timing can be staggered off that index instead
   of being written out band by band.

   The stagger is the point. A band that blooms, holds and dies while the band
   inside it is still opening reads as a flower growing in repetition rather
   than as a wheel with a fade on it.
   -------------------------------------------------------------------------- */
.mz { transform-box: view-box; transform-origin: 50% 50%; }
/* Per-band beat, derived from the index the engine stamped on the group. */
.mz { --beat: calc(var(--band, 0) * 1s); --dir: 1; }
.mz--b1, .mz--b3 { --dir: -1; }

/* kaleidoscope: counter-rotation at incommensurate rates, so the bands never
   come back into the same alignment twice. That non-repeating drift is what
   makes a kaleidoscope a kaleidoscope. */
[data-motion="kaleidoscope"] .mz--b0 { animation: mz-spin 190s linear infinite; }
[data-motion="kaleidoscope"] .mz--b1 { animation: mz-spin 247s linear infinite reverse; }
[data-motion="kaleidoscope"] .mz--b2 { animation: mz-spin 331s linear infinite; }
[data-motion="kaleidoscope"] .mz--b3 { animation: mz-spin 419s linear infinite reverse; }
[data-motion="kaleidoscope"] .mz--b4 { animation: mz-spin 523s linear infinite; }
[data-motion="kaleidoscope"] .mz--core { animation: mz-spin 97s linear infinite reverse; }
[data-motion="kaleidoscope"] .mz-rings { animation: mz-pulse 18s ease-in-out infinite; }

/* bloom: the flower opens from the centre, holds, and dies back, one band at a
   time, then does it again. Each band is nine seconds behind the one inside
   it, so at any moment the inner rings are already closing while the outer
   ones are still coming up. */
[data-motion="bloom"] .mz {
  animation: mz-bloom 34s cubic-bezier(.34,.02,.28,1) infinite;
  animation-delay: calc(var(--band, 0) * -9s);
}
[data-motion="bloom"] .mz--core { animation: mz-bloom 34s ease-in-out infinite; }
[data-motion="bloom"] .mz-rings { animation: mz-pulse 11s ease-in-out infinite; }

/* pulse: no rotation, just the beat. Reads under body copy without dragging
   the eye sideways, which rotation always does. */
[data-motion="pulse"] .mz {
  animation: mz-throb 7s ease-in-out infinite;
  animation-delay: calc(var(--band, 0) * -1.15s);
}
[data-motion="pulse"] .mz-rings { animation: mz-pulse 9s ease-in-out infinite; }

/* everything turns together, slowly: stately */
[data-motion="ceremonial"] .mz { animation: mz-spin 460s linear infinite; }
[data-motion="ceremonial"] .mz-rings { animation: mz-pulse 30s ease-in-out infinite; }

/* the ambient drift of the original */
[data-motion="drift"] .mz {
  animation: mz-spin 300s linear infinite;
  animation-duration: calc(300s + var(--band, 0) * 70s);
}
[data-motion="drift"] .mz--b1, [data-motion="drift"] .mz--b3 { animation-direction: reverse; }

/* scale pulse, no rotation at all */
[data-motion="breathe"] .mz {
  animation: mz-breathe 22s ease-in-out infinite;
  animation-delay: calc(var(--band, 0) * -3.4s);
}

/* short and punchy, then still */
[data-motion="snap"] .mz {
  animation: mz-spin 120s steps(24) infinite;
  animation-duration: calc(120s + var(--band, 0) * 30s);
}
[data-motion="snap"] .mz--b1, [data-motion="snap"] .mz--b3 { animation-direction: reverse; }

/* draws itself in once, then rests */
[data-motion="draw"] .mz {
  animation: mz-draw 2.6s cubic-bezier(.2,.7,.2,1) both;
  animation-delay: calc(var(--band, 0) * .18s);
}

/* barely moves: this design is about speed, not ornament */
[data-motion="quiet"] .mz--b2 { animation: mz-spin 720s linear infinite; }

@keyframes mz-spin   { to { transform: rotate(360deg); } }
@keyframes mz-pulse  { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
@keyframes mz-breathe{ 0%, 100% { transform: scale(1); } 50% { transform: scale(1.055); } }
@keyframes mz-draw   { from { transform: scale(.82) rotate(-14deg); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes mz-throb  { 0%, 100% { transform: scale(.94); opacity: .55; } 50% { transform: scale(1.06); opacity: 1; } }
/* Growth, hold, decay. It opens fast and dies slowly, because that is what a
   flower does and the asymmetry is what stops it reading as a throb. */
@keyframes mz-bloom {
  0%   { transform: scale(.06) rotate(-32deg); opacity: 0; }
  14%  { transform: scale(1.04) rotate(4deg);  opacity: 1; }
  22%  { transform: scale(1) rotate(0);        opacity: 1; }
  58%  { transform: scale(1.02) rotate(8deg);  opacity: .92; }
  84%  { transform: scale(.72) rotate(26deg);  opacity: .18; }
  100% { transform: scale(.06) rotate(34deg);  opacity: 0; }
}


/* The mandala bands, held still and fully visible. The bloom motion animates
   opacity from zero, so a blanket "duration .001ms" would freeze some bands at
   the invisible end of their cycle and simply delete them from the page. */
@media (prefers-reduced-motion: reduce) {
  .mz, .mz-rings, .mz--core {
    animation: none !important; transform: none !important; opacity: 1 !important;
  }
}

/* ==========================================================================
   THE SINGLE-STROKE LAYER

   Rotation is the only thing a radially symmetric drawing can do, which is why
   everything above spins: it had nothing else available to it. A sikku kolam
   and a bel are ONE continuous path, so they can be drawn instead, and drawing
   is what they look like being made.

   pathLength="1000" is set on every stroked path in the markup, so the same
   dash numbers work no matter how long the real geometry is. Nothing here has
   to know the size of the thing it is animating.
   ========================================================================== */
.kolam, .belrun { width: 100%; height: 100%; display: block; }
.kl-line { stroke-dasharray: 1000; }
.kl-dots circle { transform-box: fill-box; transform-origin: 50% 50%; }

/* -- ekrekha: one line, drawn without the pen leaving the surface ----------
   Draws on, holds while it is read, then keeps travelling until the tail
   leaves too. The pen never reverses: the line exits the way it arrived,
   which is the difference between drawing and an undo. */
[data-stroke="ekrekha"] .kl-line { animation: kl-draw 15s cubic-bezier(.5,0,.5,1) infinite; }
[data-stroke="ekrekha"] .kl-line--under { animation-delay: -.45s; opacity: .18; }
/* The pulli are set down first, the way they are on a real doorstep: dots
   first, then the line goes round them. */
[data-stroke="ekrekha"] .kl-dots circle { animation: kl-pop 15s ease-out infinite; }
[data-stroke="ekrekha"] .kl-dots circle:nth-child(3n)   { animation-delay: -.4s; }
[data-stroke="ekrekha"] .kl-dots circle:nth-child(3n+1) { animation-delay: -.8s; }

/* -- alpana: draws on and stays, then fades as a whole ---------------------
   Rice paste dries and lifts. Slower, and it dies by fading rather than by
   the line retreating. */
[data-stroke="alpana"] .kl { animation: kl-fade 21s ease-in-out infinite; }
[data-stroke="alpana"] .kl-line { animation: kl-draw-once 21s cubic-bezier(.42,0,.3,1) infinite; }

/* -- mehndi: drawn fast, then held for a long time -------------------------
   Henna goes on in seconds and sits for hours. Almost all hold. */
[data-stroke="mehndi"] .kl-line { animation: kl-draw-hold 26s cubic-bezier(.25,.6,.3,1) infinite; }
[data-stroke="mehndi"] .kl-line--under { animation-delay: -.9s; }

/* -- chalk: on, off, on, at doorstep speed --------------------------------- */
[data-stroke="chalk"] .kl-line { animation: kl-draw 9s linear infinite; }
[data-stroke="chalk"] .kl-dots circle { animation: kl-pop 9s steps(2) infinite; }

/* -- still: drawn, and left alone ------------------------------------------ */
[data-stroke="still"] .kl-line { stroke-dasharray: none; }

@keyframes kl-draw {
  0%   { stroke-dashoffset: 1000; }
  38%  { stroke-dashoffset: 0; }     /* arrived */
  62%  { stroke-dashoffset: 0; }     /* held */
  100% { stroke-dashoffset: -1000; } /* the tail follows the head out */
}
@keyframes kl-draw-once {
  0%   { stroke-dashoffset: 1000; }
  46%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: 0; }
}
@keyframes kl-draw-hold {
  0%   { stroke-dashoffset: 1000; }
  16%  { stroke-dashoffset: 0; }
  88%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -1000; }
}
@keyframes kl-fade {
  0%, 8%   { opacity: 0; }
  22%, 74% { opacity: 1; }
  100%     { opacity: 0; }
}
@keyframes kl-pop {
  0%, 2%  { transform: scale(0); opacity: 0; }
  8%      { transform: scale(1.5); opacity: 1; }
  12%, 70%{ transform: scale(1); opacity: .7; }
  100%    { transform: scale(1); opacity: 0; }
}

/* A drawing animation that never completes is a line that is never there, so
   reduced motion gets the finished drawing rather than a frozen fragment. */
@media (prefers-reduced-motion: reduce) {
  .kl, .kl-line, .kl-dots circle {
    animation: none !important; stroke-dashoffset: 0 !important;
    opacity: 1 !important; transform: none !important;
  }
}

/* -- sarpa: the snake ------------------------------------------------------
   The other modes draw the whole line on, hold it, and then let the tail
   follow the head out. That is right for a kolam, which is a closed loop
   somebody stands back from. It is wrong for a bel, which is a creeper: a
   creeper travels.

   So the dash is SHORTER than the path. A body of finite length walks the
   whole way along, head first, tail behind, and the line is never all present
   at once. The gap has to be at least as long as the dash or the body meets
   its own tail and the border reads as a dotted line instead. */
[data-stroke="sarpa"] .kl-line {
  stroke-dasharray: 360 1000;                 /* body, then a gap longer than it */
  animation: kl-snake 26s linear infinite;
}
[data-stroke="sarpa"] .kl-line--under { animation-delay: -.7s; }
[data-stroke="sarpa"] .kl-line--hi { animation-delay: .35s; }

/* Two snakes on the same border, half a cycle apart, so a long run is never
   empty for long. */
[data-stroke="sarpa2"] .kl-line {
  stroke-dasharray: 240 1000;
  animation: kl-snake 34s linear infinite;
}
[data-stroke="sarpa2"] .kl-line--under { animation-delay: -17s; opacity: .3; }

@keyframes kl-snake {
  /* Head enters at the start and leaves past the end, so both ends of the
     travel happen off the path rather than popping in and out mid-border. */
  from { stroke-dashoffset: 1360; }
  to   { stroke-dashoffset: -1000; }
}

.belband { width: 100%; overflow: hidden; line-height: 0; }
.belband > span { display: block; }
.belband svg { width: 100%; height: clamp(52px, 7vw, 104px); display: block; }

/* ==========================================================================
   The luxury arrangement.

   Gold at strength on a deep ground, cropped by the frame, one side left
   empty. The opacity here is deliberately an order of magnitude above the
   ambient washes: that treatment is for pattern sitting UNDER body copy,
   and this is pattern that is the subject.
   ========================================================================== */
.lux { position: absolute; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; }
.lux__field {
  position: absolute; top: 50%; display: block;
  width: min(150%, 1180px); aspect-ratio: 1; transform: translateY(-50%);
  opacity: calc(.42 * var(--pattern-intensity, 1));
  animation: spin 420s linear infinite;
}
/* Half off the edge. The crop is the whole point: an ornament that fits
   inside its box reads as a sticker. */
.lux--right .lux__field { right: -34%; }
.lux--left  .lux__field { left: -34%; }

.lux__medal {
  position: absolute; top: 50%; display: block;
  width: clamp(130px, 17vw, 250px); aspect-ratio: 1;
  transform: translateY(-50%);
  opacity: calc(.85 * var(--pattern-intensity, 1));
  border-radius: 50%;
  background: radial-gradient(circle at 38% 32%,
    color-mix(in srgb, var(--accent) 40%, var(--paper)) 0%,
    color-mix(in srgb, var(--accent) 22%, var(--paper)) 62%,
    color-mix(in srgb, var(--ink) 14%, var(--paper)) 100%);
  box-shadow: 0 14px 40px -14px color-mix(in srgb, var(--ink) 70%, transparent);
  animation: spin 190s linear infinite reverse;
}
.lux--right .lux__medal { right: 26%; }
.lux--left  .lux__medal { left: 26%; }
.lux__field svg, .lux__medal svg, .lux__konia svg { width: 100%; height: 100%; display: block; }

.lux__konia {
  position: absolute; display: block; width: clamp(58px, 8vw, 116px); aspect-ratio: 1;
  opacity: calc(.55 * var(--pattern-intensity, 1));
}
.konia--tl { position: absolute; inset: 14px auto auto 14px; }
.konia--tr { position: absolute; inset: 14px 14px auto auto; }
.konia--bl { position: absolute; inset: auto auto 14px 14px; }
.konia--br { position: absolute; inset: auto 14px 14px auto; }
.lux__konia:has(.konia--tl) { inset: 14px auto auto 14px; }
.lux__konia:has(.konia--tr) { inset: 14px 14px auto auto; }
.lux__konia:has(.konia--bl) { inset: auto auto 14px 14px; }
.lux__konia:has(.konia--br) { inset: auto 14px 14px auto; }

@media (prefers-reduced-motion: reduce) {
  .lux__field, .lux__medal { animation: none !important; }
}
