@charset "UTF-8";

/* ═══════════════════════════════════════════════════════════════════════════════
   Site stylesheet — loaded after bootstrap.min.css.

   Overrides go through Bootstrap's own variables wherever Bootstrap has one
   (--bs-*, and component variables like --bs-btn-*, --bs-list-group-*); plain rules
   only where it doesn't.

   Sections: tokens · base · links · typography · article bodies · buttons ·
   badges & breadcrumbs · header & sidebar · cards & accordion · reading ceremony ·
   image backdrop & promo · utilities.
   ═══════════════════════════════════════════════════════════════════════════════ */


/* ─── Tokens ─────────────────────────────────────────────────────────────────── */

:root {
  /* Body text. No web font: the platform's own UI face (Roboto on Android, SF on
     Apple, Segoe UI on Windows). */
  --bs-font-sans-serif: Roboto, system-ui, -apple-system, "Segoe UI", Ubuntu, Cantarell, "Noto Sans", "Helvetica Neue", Arial, sans-serif;
  --bs-body-font-size: 1.0625rem;   /* 17px */
  --bs-body-line-height: 1.53;      /* 26px */
  --bs-body-bg: #eff0f2;

  --bs-primary-text-emphasis: var(--bs-accordion-btn-color);

  --bs-custom-link-color: 0,0,0;
  --bs-custom-link-hover-color: 65,65,65;
  --bs-link-color-rgb: var(--bs-custom-link-color);
  --bs-link-hover-color-rgb: var(--bs-custom-link-hover-color);

  /* Type scale beyond the body. */
  --type-small-size: 0.9375rem;     /* 15px */
  --type-gap: 0.75rem;              /* 12px: paragraph → paragraph, heading → text */
  --type-gap-before-heading: 1.25rem; /* 20px: text → heading */

  /* Hover/current fill for clickable rows: sidebar topics and reading cards. */
  --hover-bg: var(--bs-tertiary-bg);

  /* Brand accent: focus ring, skip link, article callouts. */
  --accent: #2481cc;
  --accent-subtle: #f4f8fc;

  /* "Plop": a damped spring for a card settling back to its size — one sink past its
     rest and a faint bob, in one motion. Sampled from the spring equation (damping
     0.33, 1.5 swings over the duration) into CSS linear(); browsers without linear()
     fall back to a plain ease. @keyframes fortuneLift repeats the curve inline: a
     var() there is not applied to a keyframe's timing function. */
  --ease-plop: linear(0, 0.120, 0.404, 0.740, 1.037, 1.239, 1.327, 1.316, 1.236, 1.126, 1.020, 0.940, 0.897, 0.890, 0.910, 0.944, 0.981, 1.012, 1);
  /* Backdrop wash — the blurred, bleached picture behind a block. One set of values
     for every place that uses it (the fortune block, the readings promo), so the two
     can never drift apart. See "Image backdrop" below.

     Contrast and brightness are a pair, not two independent dials: together they mix
     the picture toward white by a factor t, with contrast = (1-t)/(1+t) and
     brightness = 1+t. Here t = 0.35. Raising brightness alone would brighten and
     harden the image at once, which is the thing this avoids. */
  --backdrop-blur: 60px;
  --backdrop-saturate: 1.15;
  --backdrop-contrast: 0.48;
  --backdrop-brightness: 1.35;
  --backdrop-opacity: 0.45;
}


/* ─── Base ───────────────────────────────────────────────────────────────────── */

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

*:focus:not(:focus-visible) {
  box-shadow: none !important;
  outline: 0;
}

/* The rule above kills the focus ring for mouse users, but it also strips decorative
   drop shadows. Re-assert shadow-sm for opted-in elements once focus settles — but NOT
   while actively pressed (:active), so the shadow dips on press and returns on release. */
.shadow-sm:focus:not(:focus-visible):not(:active) {
  box-shadow: var(--bs-box-shadow-sm) !important;
}

*:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  z-index: 9999;
  padding: 0.75rem 1.5rem;
  background: var(--accent);
  color: #fff;
  font-weight: bold;
  text-decoration: none;
}

.skip-link:focus {
  top: 0;
}

::selection {
  background-color: black;
  color: white;
}

.container {
  max-width: 1200px;
}


/* ─── Links ──────────────────────────────────────────────────────────────────── */

/* No underline, except inside running text where nothing else would mark a link.
   Clickable things dim slightly on hover (devices that can hover). */
a {
  text-decoration: none;
  transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}

@media (hover: hover) {
  a:not(.btn, .badge, .list-group-item, .dropdown-item, :has(> .img-container-mini)):hover {
    opacity: 0.72;
  }

  /* A reading card (partials/reading-card.php — the link holding .img-container-mini)
     lights up a grey tile with a border and shadow-sm's shadow instead of fading, the
     way sidebar topics and pills answer. The tile's room is the link's own p-2,
     taken from the lists around it (p-2 p-md-3, g-2), and the border is a 1px shadow
     ring rather than a border, so nothing moves when it appears. */
  a:has(> .img-container-mini):hover {
    background-color: var(--hover-bg);
    box-shadow: 0 0 0 1px var(--bs-border-color), var(--bs-box-shadow-sm);
  }
}

/* The card's text keeps its colour on hover: Bootstrap's a:hover swaps in the hover
   link colour, so the card's hover colour is pointed back at the normal one. */
a:has(> .img-container-mini) {
  --bs-link-hover-color-rgb: var(--bs-custom-link-color);
  transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

/* A link inside running text — no class, which is what markdown and hand-written
   copy produce — keeps its underline: same colour as the text, nothing else shows it.
   A FAQ answer is running text too, though templates put it in .accordion-body
   without a <p>. */
:is(p, li, dd, td, blockquote, .accordion-body) a:not([class]) {
  text-decoration: underline;
}

/* A card link that opens a preview popover instead of navigating (app.js
   initCardPopovers): dotted, the usual mark for "explains, doesn't take you away". */
:is(p, li, dd, td, blockquote, .accordion-body) a[data-card-popover]:not([class]) {
  text-decoration-style: dotted;
  text-underline-offset: 0.2em;
}


/* ─── Typography ─────────────────────────────────────────────────────────────── */

/* Weight is not set here: templates choose it with Bootstrap's fw-* utilities.
   Every rule names the tag and its Bootstrap class twin (.h1–.h6) together, so a
   heading styled by class looks exactly like the tag — Bootstrap's own .h* carry
   its RFS sizes, which this scale replaces.
   Balanced wrapping for h2–h6 only; h1 wraps as normal text, filling each line. */
h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
  text-wrap: balance;
}

/* Fixed sizes, the same on phone and desktop (no Bootstrap RFS scaling). Headings get
   a tighter line than running text: they are large and short. */
h1, .h1, h2, .h2 { font-size: 1.35rem;   line-height: 1.625rem; }  /* 21.6 / 26 */
h3, .h3, h4, .h4 { font-size: 1.125rem;  line-height: 1.5rem; }    /* 18 / 24 */
h5, .h5          { font-size: 1.0625rem; line-height: 1.625rem; }  /* 17 / 26 */
h6, .h6          { font-size: 0.9375rem; line-height: 1.375rem; }  /* 15 / 22 */

p {
  margin-bottom: var(--type-gap);
}

/* On a phone, 17px text in a ~340px column leaves a ragged right edge wherever a
   long Russian word will not fit; hyphenation (by <html lang>) evens it out. */
@media (max-width: 575.98px) {
  p, li {
    -webkit-hyphens: auto;
    hyphens: auto;
  }
}

/* Small and lead text are running paragraphs too — only the size differs, so they
   take the body's line-height ratio rather than a fixed height of their own, and
   follow --bs-body-line-height if it ever changes. */
.small, small {
  font-size: var(--type-small-size);
  line-height: var(--bs-body-line-height);
}

.lead {
  font-size: 1.125rem;              /* 18px */
  font-weight: 400;
  line-height: var(--bs-body-line-height);
}

.fs-6 {
  font-size: var(--bs-body-font-size) !important;
}

@media (min-width: 768px) {
  .no-small-md {
    font-size: var(--bs-body-font-size) !important;
  }
}


/* ─── Article bodies (markdown) ──────────────────────────────────────────────── */

/* .page-body wraps rendered markdown — static pages and every guide/combination
   article block. Markdown tags carry no classes; what utilities can express is added
   in PHP (Template::articleHtml(): lists, tables, h3 spacing), the rest lives here. */

/* h2 only reaches a body on static pages: guide articles are split into sections at
   their h2s, which the templates render as section headers. */
.page-body h2 {
  font-size: 1.125rem;
  line-height: 1.5rem;
  font-weight: bold;
  margin-top: var(--type-gap-before-heading);
  margin-bottom: var(--type-gap);
}

.page-body :is(ul, ol) {
  margin-bottom: var(--type-gap);
}

.page-body li + li {
  margin-top: 0.375rem;
}

.page-body > :first-child {
  margin-top: 0;
}

/* Callouts: a markdown blockquote (> …) and the intro's lead paragraph wear the same
   tip box — accent rule on the left, a tint of the accent behind. */
.page-body :is(blockquote, .lead) {
  border-left: 4px solid var(--accent);
  background: var(--accent-subtle);
  padding: 0.75rem 1rem;
  border-radius: 0.25rem;
}

.page-body blockquote {
  margin: 0 0 1rem;
}

.page-body blockquote > :last-child {
  margin-bottom: 0;
}


/* ─── Buttons ────────────────────────────────────────────────────────────────── */

/* Sizes as Bootstrap has them — on .btn and its size modifiers, never on a colour
   class, so any colour can go on any button without its size moving. Each a step
   of the type scale: .btn 17/24 (42px tall) — every action, dark or white, and the
   heart beside them; .btn-sm 15/22 (34px); .btn-lg 17/26 (50px). Actions are
   medium weight. Small navigation is a pill — our own variant, .btn-pill (below).
   The modifiers have to be restated here at all — .btn above sets the same
   variables with the same specificity after bootstrap.min.css, so Bootstrap's own
   .btn-lg/.btn-sm would lose to it. */
.btn {
  --bs-btn-font-size: 1.0625rem;    /* 17px */
  --bs-btn-line-height: 1.5rem;     /* 24px */
  --bs-btn-padding-y: 0.5rem;       /* 8px → 42px tall */
  --bs-btn-padding-x: 0.875rem;     /* 14px */
  --bs-btn-font-weight: 500;
}

.btn-lg {
  --bs-btn-font-size: 1.0625rem;    /* 17px */
  --bs-btn-line-height: 1.625rem;   /* 26px */
  --bs-btn-padding-y: 0.75rem;      /* 12px → 50px tall */
  --bs-btn-padding-x: 1.25rem;      /* 20px */
}

.btn-sm {
  --bs-btn-font-size: 0.9375rem;    /* 15px */
  --bs-btn-line-height: 1.375rem;   /* 22px */
  --bs-btn-padding-y: 0.3125rem;    /* 5px → 34px tall */
  --bs-btn-padding-x: 0.875rem;     /* 14px */
}

/* Card preview popover (app.js initCardPopovers) — the only popover on the site:
   half of Bootstrap's 1rem body padding, the card image and button need little air. */
.popover {
  --bs-popover-bg: #fff;            /* Bootstrap's default is the page's grey body-bg */
  --bs-popover-body-padding-x: 0.5rem;
  --bs-popover-body-padding-y: 0.5rem;
}

/* Bootstrap fills a dropdown menu with the page background, which here is grey;
   menus sit on white like the pills that open them. A pressed item goes black
   like the current pill, not Bootstrap's blue. */
.dropdown-menu {
  --bs-dropdown-bg: #fff;
  --bs-dropdown-link-active-bg: var(--bs-dark);
  --bs-dropdown-link-active-color: #fff;
}

/* Collapsible text (partials/collapsible-text.php): the paragraph is clamped to six
   lines, ending in the browser's ellipsis, for as long as the "show more" button
   right after it is closed. */
p:has(+ .show-more[aria-expanded="false"]) {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 6;
  line-clamp: 6;
  overflow: hidden;
}

/* Once it has opened what it controls, a "show more" has nothing left to do. */
.show-more[aria-expanded="true"] {
  display: none;
}

/* "Show more" under a collapsed paragraph: a plain-text button. */
.show-more {
  font-weight: bold;
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  font-size: inherit;
  font-family: inherit;
  color: inherit;
  display: inline;
}


/* ─── Pills & breadcrumbs ────────────────────────────────────────────────────── */

/* Pill — the site's small rounded navigation chip (breadcrumbs, topic and method
   links, switchers, category and section anchors, card facts, counts). Our own
   button variant, made the Bootstrap way — only its --bs-btn-* variables — so every
   state comes from Bootstrap's .btn, nothing of Bootstrap is overridden:
     class="btn btn-pill shadow-sm"             white; light grey under the pointer
     … + active   (and aria-current="page")      dark — "you are here"
     … + disabled (a span, aria-disabled="true") faded
   Sizes the Bootstrap way: plain is body text, 17/24, 34px tall (the main menu and
   the method switcher); + btn-sm is small text, 15/22, 32px (every other pill). Regular weight —
   navigation, not an action. shadow-sm stays in the markup, like on every button. */
.btn-pill {
  --bs-btn-font-size: var(--bs-body-font-size);
  --bs-btn-line-height: 1.5rem;     /* 24px */
  --bs-btn-padding-y: 0.25rem;      /* 4px → 34px tall */
  --bs-btn-padding-x: 0.875rem;     /* 14px */
  --bs-btn-font-weight: 400;
  --bs-btn-border-radius: var(--bs-border-radius-pill);
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-bg: #fff;
  --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-hover-color: var(--bs-body-color);
  --bs-btn-hover-bg: var(--bs-light);
  --bs-btn-hover-border-color: var(--bs-border-color);
  --bs-btn-active-color: var(--bs-body-color);     /* the press: a shade darker */
  --bs-btn-active-bg: var(--bs-secondary-bg);
  --bs-btn-active-border-color: var(--bs-border-color);
  --bs-btn-disabled-color: var(--bs-body-color);
  --bs-btn-disabled-bg: #fff;
  --bs-btn-disabled-border-color: var(--bs-border-color);
  --bs-btn-disabled-opacity: 0.5;
  --bs-btn-focus-shadow-rgb: var(--bs-dark-rgb);
  white-space: nowrap;
}

/* Small: tighter than the site's .btn-sm (34px) — a pill is navigation, packed in
   rows, and 32px matches the breadcrumb strip's rhythm. */
.btn-pill.btn-sm {
  --bs-btn-font-size: var(--type-small-size);
  --bs-btn-line-height: 1.375rem;   /* 22px */
  --bs-btn-padding-y: 0.25rem;      /* 4px → 32px tall */
  --bs-btn-padding-x: 0.75rem;      /* 12px */
}

/* Current: dark, in every state — it is where you are, pressing it changes nothing. */
.btn-pill.active {
  --bs-btn-color: #fff;
  --bs-btn-bg: var(--bs-dark);
  --bs-btn-border-color: var(--bs-dark);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--bs-dark);
  --bs-btn-hover-border-color: var(--bs-dark);
  --bs-btn-active-color: #fff;
  --bs-btn-active-bg: var(--bs-dark);
  --bs-btn-active-border-color: var(--bs-dark);
}

/* The pill opening the methods' dropdown stays white while its menu is open —
   Bootstrap marks the open toggle .show and paints it with the active colours. */
.dropdown > .btn-pill.show {
  --bs-btn-active-color: var(--bs-body-color);
  --bs-btn-active-bg: #fff;
}

/* A crumb never wider than its <li>; past that, text-truncate cuts it with an
   ellipsis. `display: block` so the percentage resolves against the <li> and the
   ellipsis has a box to cut against. */
.crumb {
  display: block;
  max-width: 100%;
}

/* Optical centring of the label. A font's line box is not symmetric around the
   letters — Segoe UI keeps more room above them than below — so with equal padding
   the text sits low and the bottom gap looks smaller. text-box trims the line box to
   cap height and baseline; the padding then grows by exactly what was trimmed,
   (1lh − 1cap) / 2 per side, so every button and badge keeps its height.
   Browsers without text-box (Firefox for now) skip the whole block unchanged. */
@supports (text-box: trim-both cap alphabetic) {
  .btn,
  .badge {
    text-box: trim-both cap alphabetic;
  }

  .btn {
    padding-block: calc(var(--bs-btn-padding-y) + (1lh - 1cap) / 2);
  }

  .badge {
    padding-block: calc(var(--bs-badge-padding-y) + (1lh - 1cap) / 2);
  }
}

/* Breadcrumb strip. On md+ it stands in one row with the sidebar's header, whose
   single line of text is 26px; a pill is 32px, so the strip loses 3px of
   padding top and bottom to come out at the same height and the two bottom borders
   line up. Below md the sidebar is not beside it — plain 1rem. The main menu's strip
   holds the same badges and takes the same padding. */
nav[aria-label="Breadcrumb"],
.navbar .offcanvas .offcanvas-body {
  padding-block: 1rem;
}

@media (min-width: 768px) {
  nav[aria-label="Breadcrumb"],
  .navbar .offcanvas .offcanvas-body {
    padding-block: calc(1rem - 3px);
  }
}

/* Main menu in the desktop row (xl up): the topics are bold words, not chips —
   no border and no shadow until one is current, which stays the dark pill. Without
   an outline the pill's side padding would only push the words apart, so it is
   trimmed. The shadow comes from the shadow-sm utility, so it is switched off
   through its variable. Below xl, in the offcanvas, they stay ordinary pills.
   Bootstrap has no responsive fw-* or shadow utilities. */
@media (min-width: 1200px) {
  .navbar .btn-pill {
    --bs-btn-font-weight: 700;
    --bs-btn-padding-x: 0.625rem;     /* 10px */
  }
  .navbar .btn-pill:not(.active) {
    --bs-btn-border-color: transparent;
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-border-color: transparent;
    --bs-box-shadow-sm: none;
  }
}

/* The trail stays on one line; this lets the visible crumbs shrink below their text
   width so their ellipsis can kick in (a flex item won't go under min-content). */
nav[aria-label="Breadcrumb"] li {
  min-width: 0;
}


/* ─── Header & sidebar ───────────────────────────────────────────────────────── */

/* Logo wordmark: a fixed 24px (fs-3 would scale with the viewport), on lh-sm's
   1.25 → 30px line in the template, so the header is no taller than it needs to be. */
.navbar {
  --bs-navbar-padding-y: 1rem;
  --bs-navbar-brand-padding-y: 0;
  --bs-navbar-brand-font-size: 1.5rem;
  --bs-navbar-color: rgb(var(--bs-custom-link-color));
  --bs-navbar-hover-color: rgb(var(--bs-custom-link-hover-color));
}

@media (min-width: 1200px) {
  .no-gutter-x {
    margin-right: calc(-0.25 * var(--bs-gutter-x));
    margin-left: calc(-0.25 * var(--bs-gutter-x));
    max-width: none;
  }
}

/* Sidebar topics (a list-group): black text; hovering a row and the current topic
   both get the same light grey — background only, no weight change — instead of
   list-group's blue. */
.sidebar {
  --bs-list-group-bg: var(--bs-white);  /* default is the page's grey body-bg */
  --bs-list-group-item-padding-y: 0.75rem;
  --bs-list-group-action-color: var(--bs-body-color);
  --bs-list-group-action-hover-color: var(--bs-body-color);
  --bs-list-group-action-hover-bg: var(--hover-bg);
  --bs-list-group-action-active-color: var(--bs-body-color);
  --bs-list-group-action-active-bg: var(--hover-bg);
  --bs-list-group-active-color: var(--bs-body-color);
  --bs-list-group-active-bg: var(--hover-bg);
  --bs-list-group-active-border-color: var(--bs-border-color);
}


/* ─── Cards & accordion ──────────────────────────────────────────────────────── */

/* Tile cards (card-meaning thumbnails + combination pairs) are light grey all over;
   the card paints it, so its body needs no fill (or radius fix) of its own. */
.card {
  --bs-card-bg: var(--bs-light);
}

.accordion {
  --bs-accordion-bg: #fff;
  --bs-accordion-btn-bg: #fff;
  --bs-accordion-active-bg: var(--bs-light);
  --bs-accordion-active-color: var(--bs-body-color);
  --bs-accordion-btn-padding-x: 1.5rem;
  --bs-accordion-body-padding-x: 1.5rem;
}

/* FAQ questions read as titles: body size, and the body's line-height rather than
   the heading's they sit in. */
.accordion-button {
  font-size: var(--bs-body-font-size);
  line-height: var(--bs-body-line-height);
}

.img-container-mini {
  width: 100px;
  height: 100px;
  object-fit: cover;
}

.lines-count-4 {
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.type-icon {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
}

.type-icon svg {
  width: 1.15em;
  height: 1.15em;
  vertical-align: middle;
  margin-top: -0.1em;
}

/* Guide card page: fixed intro image width so the gap to the
   characteristics list is identical across card types (tarot, cards,
   lenormand, runes), regardless of each image's native aspect ratio. */
.guide-card-intro-img {
  width: 160px;
}

/* On a phone the facts stay beside the card, so the card gives up some width. */
@media (max-width: 575.98px) {
  .guide-card-intro-img {
    width: 120px;
  }
}

/* Icon placeholder ("any other card") in the combinations hub tile — matches
   the 48x84 card thumbnail beside it. */
.combo-card-back {
  width: 48px;
  height: 84px;
  transform: rotateZ(10deg);
}

/* Breathing room when jumping to a category section via the anchor pills. */
.combo-cat {
  scroll-margin-top: 1rem;
}

.breathing-image-container {
  overflow: hidden;
  position: relative;
}

.breathing-image-container img {
  width: 100%;
  height: auto;
  display: block;
  animation: breathingZoom 10s ease-in-out infinite;
  transform-origin: center center;
}

@keyframes breathingZoom {
  0% { transform: scale(1); }
  50% { transform: scale(1.07); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .breathing-image-container img {
    animation: none;
  }
}


/* ─── Reading ceremony (fortune) ─────────────────────────────────────────────── */

/* The reading screens' subtitles wrap like the headings above them — lines balanced
   rather than the last one left short (no Bootstrap utility for text-wrap: balance). */
.fortune-picker p {
  text-wrap: balance;
}

ol.fortune-questions {
  counter-reset: fortune-counter;
  list-style: none;
  padding-left: 0;
  margin-top: 0.25rem;
  margin-bottom: 0.25rem;
}

ol.fortune-questions li {
  position: relative;
  padding-left: 3rem;
  margin-bottom: 2rem;
}

ol.fortune-questions li::before {
  counter-increment: fortune-counter;
  content: counter(fortune-counter);
  position: absolute;
  left: 0;
  top: 0;
  width: 2.2rem;
  height: 2.2rem;
  background-color: rgb(var(--bs-secondary-bg-rgb));
  border-radius: 50%;
  text-align: center;
  font-weight: bold;
  font-size: 1.25rem;
  box-shadow: var(--bs-box-shadow-sm) !important;
  border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
}

ol.fortune-questions li:last-child, ol.fortune-questions li:last-child p {
  margin-bottom: 0rem;
}

/* Step number above a question — in the picker's title and in the result. The
   heading around it is a centred column (d-flex flex-column align-items-center
   gap-2); the circle centres its own digit as a flex box.
   Sized through a custom property because the result uses a smaller one. */
.fortune-step-number {
  --fortune-badge: 2.2rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--fortune-badge);
  height: var(--fortune-badge);
  background-color: var(--bs-dark);
  color: #fff;
  border-radius: 50%;
  font-weight: bold;
  font-size: 1.25rem;
  box-shadow: var(--bs-box-shadow-sm) !important;
}

.fortune-item .fortune-step-number {
  --fortune-badge: 1.75rem;
  font-size: 1rem;
}

/* Card grid: --fortune-cols per row — 8 for the pick grid; the short rows (start
   screen, picked cards) set fewer from fortune.js, so their cards come out larger,
   and on a phone may set --fortune-cols-narrow to fill the width. The gap lives in
   a custom property because each tile's flex-basis has to subtract its share of it. */
.fortune-grid {
  --fortune-cols: 8;
  --fortune-cols-now: var(--fortune-cols);
  --fortune-gap: 1rem;
  gap: var(--fortune-gap);
}

@media (max-width: 575.98px) {
  .fortune-grid {
    --fortune-cols-now: var(--fortune-cols-narrow, var(--fortune-cols));
    --fortune-gap: 0.45rem;
  }
}

.fortune-grid > * {
  flex: 0 0 calc(100% / var(--fortune-cols-now) - var(--fortune-gap) * (var(--fortune-cols-now) - 1) / var(--fortune-cols-now));
}

/* Riffle-shuffle animation: the deck arcs from one side to the other and back,
   each card lagging behind the last, the way a dealer passes them hand to hand. */
.fortune-shuffle {
  position: relative;
  height: 135px;
}

.fortune-shuffle-card {
  position: absolute;
  top: 50%;
  left: 50%;
  height: 90px;
  width: auto;
  margin: -45px 0 0 -30px;
  border-radius: 4px;
  /* Two animations on one element: the card fades up while it is already
     travelling, so it joins the shuffle mid-motion instead of waiting its turn.
     `backwards` keeps it hidden during its delay; once the fade is done the
     element falls back to this rule's own opacity rather than to a held keyframe. */
  opacity: 1;
  /* Linear, not ease-in-out: easing stalls every card at the turns, where they
     pile onto each other instead of staying strung out along the path. */
  animation: fortunePass 2.2s linear infinite,
             fortuneAppear 0.5s ease-out backwards;
}

@keyframes fortuneAppear {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* An arc, not a slide: the card lifts as it leaves one hand and settles into the
   other, shrinking and dimming at the far end so the stream reads as depth. */
@keyframes fortunePass {
  0%   { transform: translate(-105px, 12px) rotate(-25deg) scale(0.75); filter: brightness(0.82); }
  25%  { transform: translate(0, -18px) rotate(0deg) scale(1.2); filter: brightness(1); }
  50%  { transform: translate(105px, 12px) rotate(25deg) scale(0.75); filter: brightness(0.82); }
  75%  { transform: translate(0, -18px) rotate(0deg) scale(1.2); filter: brightness(1); }
  100% { transform: translate(-105px, 12px) rotate(-25deg) scale(0.75); filter: brightness(0.82); }
}

/* Card flip + shine for the reading picker. Bootstrap has no 3D-transform
   utilities, and the shine needs a pseudo-element. */
/* A transition runs with the timing of the state it goes TO: coming off hover the
   card settles with the plop (sinks under its size, bobs, rests), going up to hover
   it rises plainly. */
.fortune-tile {
  perspective: 1000px;
  transition: transform 0.6s var(--ease-plop), box-shadow 0.3s ease;
}

/* The readings promo's topic pills borrow the same lift (and the idle
   invitation, invite() in app.js via [data-invite]). Their shadow-sm is !important, so the shadow
   stays put and only the scale moves. The colour fades are .btn's own, kept. */
[data-invite] > .btn-pill {
  transition: transform 0.6s var(--ease-plop), color 0.15s ease-in-out,
              background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}

[data-invite] > .btn-pill:hover {
  transition: transform 0.3s ease, color 0.15s ease-in-out,
              background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
  transform: scale(1.08);
}

.fortune-tile:not(:disabled):hover {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  transform: scale(1.08);
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25);
  z-index: 1;
}

.fortune-tile-inner {
  position: relative;
  width: 100%;
  transform-style: preserve-3d;
  transition: transform 0.6s;
}

.fortune-tile.is-flipped .fortune-tile-inner {
  transform: rotateY(180deg);
}

.fortune-tile-back,
.fortune-tile-front {
  backface-visibility: hidden;
}

.fortune-tile-front {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  transform: rotateY(180deg);
}

/* The card's spirit: a copy that rushes at the viewer and fades as the tile turns.
   Drawn on <body>, positioned over the tile — the tile clips its own contents, so
   this could not grow past its edges from inside. fortune.js places and removes it. */
.fortune-ghost {
  position: fixed;
  pointer-events: none;
  z-index: 1080;
  animation: fortuneGhost 0.4s ease-out forwards;
}

@keyframes fortuneGhost {
  from { transform: scale(1);   opacity: 0.85; }
  to   { transform: scale(2.4); opacity: 0; }
}

.fortune-tile.is-shining::after {
  content: "";
  position: absolute;
  top: 0;
  left: -75%;
  width: 50%;
  height: 100%;
  background: linear-gradient(120deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.6) 50%, rgba(255,255,255,0.1) 100%);
  transform: skewX(-25deg);
  animation: fortuneShine 0.6s ease forwards;
  pointer-events: none;
  border-radius: var(--bs-border-radius);
}

@keyframes fortuneShine {
  to { left: 125%; }
}

/* Idle invitation on the start screen — the two moves played on the face-down
   cards while they wait, and on the readings promo's badges (see invite() in
   app.js for the running order):
   the lift is the hover effect itself, the hop sends the row up in a wave. */
/* The hover lift, played in two strokes: a brisk but smooth rise (about a third of
   the time, easing in gently and out softly), then a slower settle with the same plop as coming off hover — the
   curve itself does the sink and the bob, so the card never stalls on the way. The
   curve is --ease-plop written out: var() does not work for a keyframe's timing
   function. */
@keyframes fortuneLift {
  0%   { transform: none; box-shadow: none; animation-timing-function: cubic-bezier(0.33, 0, 0.2, 1); }
  35%  { transform: scale(1.08); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25); animation-timing-function: linear(0, 0.120, 0.404, 0.740, 1.037, 1.239, 1.327, 1.316, 1.236, 1.126, 1.020, 0.940, 0.897, 0.890, 0.910, 0.944, 0.981, 1.012, 1); }
  100% { transform: none; box-shadow: none; }
}

/* A real hop, not a float: a slight crouch, up, the landing sinks below the line and
   the bounce dies out — a damped swing, eased like a sine so it never stalls. */
@keyframes fortuneHop {
  0%   { transform: none; }
  12%  { transform: translateY(3px); }
  40%  { transform: translateY(-12px); }
  65%  { transform: translateY(4px); }
  83%  { transform: translateY(-1.5px); }
  100% { transform: none; }
}


/* ─── Image backdrop & promo ─────────────────────────────────────────────────────
   A picture blurred past recognition and bleached, behind a block's content: the
   reading's own hero on the fortune block, the divination type's on the readings
   promo. The image comes in through --backdrop-image, set inline because it differs
   per page. Self-contained — drop this section and the `image-backdrop` class where
   it is used.

   The layer sits at z-index 0 with the content lifted above it, rather than being
   pushed to -1 beneath: negative stacking depends on the block owning a stacking
   context, while lifting the content is unconditional.

   `inset` is negative because blur samples past an element's edge — without the
   overhang the wash fades out just short of the corners. */
.image-backdrop {
  position: relative;
  overflow: hidden;
}

.image-backdrop > * {
  position: relative;
  z-index: 1;
}

.image-backdrop::before {
  content: "";
  position: absolute;
  inset: -18%;
  z-index: 0;
  pointer-events: none;
  background-image: var(--backdrop-image);
  background-size: cover;
  background-position: center;
  /* Blur first so nothing of the picture survives, then mix it toward white. The
     numbers live on :root — see the note there for why contrast and brightness move
     together, and change them there so every block that uses this stays in step. */
  filter:
    blur(var(--backdrop-blur))
    saturate(var(--backdrop-saturate))
    contrast(var(--backdrop-contrast))
    brightness(var(--backdrop-brightness));
  opacity: var(--backdrop-opacity);
  transition: opacity 0.6s ease;
}

/* Once the reading itself is showing, long text sits on plain white: the wash fades
   out entirely. Keys off the spread being visible, no class of its own. */
.image-backdrop:has(> .fortune-result:not([hidden])) {
  --backdrop-opacity: 0;
}

/* Readings promo backdrop — a flat gradient rather than the blurred photograph the
   fortune block uses. Sampled off a reference: warm beige at the top left, a paler
   cream band about a third of the way across, cool blue-grey at the bottom right,
   the whole thing a shade deeper toward the bottom.

   No pseudo-element or blur needed here — it is a background, so it already sits
   under the content. The radial lays the cream band over the diagonal blend. */
.promo-gradient {
  background-image:
    radial-gradient(62% 92% at 28% 55%, rgba(245, 238, 226, 0.92) 0%, rgba(245, 238, 226, 0) 70%),
    linear-gradient(115deg, #f1e7de 0%, #ece6db 30%, #e3e5e2 58%, #d3dfe3 85%, #ccdade 100%);
}

/* The card lying behind the readings promo. On a phone the ticker titles run across
   it, so it steps back; on wider screens they end well short of it. Bootstrap's
   opacity utilities have no breakpoint variants. */
@media (max-width: 767.98px) {
  .promo-gradient > .z-n1 { opacity: 0.5; }
}

/* Rotating reading titles under the promo badges.

   Both layers of a row occupy the same grid cell, so they overlap without either
   leaving the flow: the row is still as tall as its content, and the outgoing title
   can fade while the incoming one is already arriving. Absolute positioning would
   collapse the row's height; a single element could not cross-fade at all, since it
   can only hold one title.

   Bootstrap's own .fade is 0.15s, tuned for popovers appearing on click — text
   drifting by itself wants longer. */
.reading-ticker-row {
  display: grid;
  /* Grid stretches its items by default, which made the link span the whole row and
     catch clicks well past the end of the title. Sized to the text instead. */
  justify-items: start;
}

.reading-ticker-row > a {
  grid-area: 1 / 1;
  /* text-truncate has nothing to ellipsise against once the box is content-sized;
     this gives it the row's width as the ceiling. */
  max-width: 100%;
  transition: opacity 0.7s ease;
}

.reading-ticker-row > a.is-hidden {
  opacity: 0;
  pointer-events: none;
}

/* Home page quiz (guide-hub.php, initGuideQuiz() in app.js). The options are bare
   frames on the block's gradient — a border, no fill. Hover only darkens the frame.
   Once a pick is made (and held a beat — see .is-picked) the right answer's frame
   and text ease to green, a wrong pick's to red, the rest step back. aria-disabled
   rather than disabled, so Bootstrap's faded :disabled look doesn't wash out the
   answer being shown. The card image is 72px wide — a reading's start-screen tile. */
.guide-quiz-option {
  --bs-btn-bg: transparent;
  --bs-btn-border-color: rgba(var(--bs-dark-rgb), 0.25);
  --bs-btn-hover-bg: transparent;
  --bs-btn-hover-border-color: var(--bs-dark);
  --bs-btn-active-bg: transparent;
  --bs-btn-active-border-color: var(--bs-dark);
  transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, opacity 0.4s ease;
}

.guide-quiz-option[aria-disabled] {
  pointer-events: none;
}

/* The pick taken, before the verdict (PICK_HOLD_MS in app.js): only its frame
   darkens, so for a moment nothing says right or wrong yet. */
.guide-quiz-option.is-picked {
  --bs-btn-border-color: var(--bs-dark);
}

/* The right answer is the one option that gets a fill: the frames stay bare while
   the question is open, and the result is shown once, where the eye should go. */
.guide-quiz-option.is-right {
  --bs-btn-bg: var(--bs-success-bg-subtle);
  --bs-btn-border-color: var(--bs-success);
  --bs-btn-color: var(--bs-success-text-emphasis);
}

/* A card tile has no frame to colour (border-0), so a wrong pick gets the tint
   the right answer has, in red. */
.guide-quiz-option.border-0.is-wrong {
  --bs-btn-bg: var(--bs-danger-bg-subtle);
}

.guide-quiz-option.is-wrong {
  --bs-btn-border-color: var(--bs-danger);
  --bs-btn-color: var(--bs-danger-text-emphasis);
}

/* Tick and cross (initGuideQuiz() adds them after the pick), in the full colour —
   the text beside them already carries the darker emphasis shade. */
.guide-quiz-mark {
  font-size: 1.125rem;
  animation: quizMarkIn 0.45s var(--ease-plop) both;
}

/* Grows in with the same damped spring the reading's cards settle on. */
@keyframes quizMarkIn {
  from { opacity: 0; transform: scale(0.6); }
  to   { opacity: 1; transform: none; }
}

.is-right > .guide-quiz-mark { color: var(--bs-success); }
.is-wrong > .guide-quiz-mark { color: var(--bs-danger); }

/* The rest step back after the verdict has landed, not with it. */
.guide-quiz-option.is-other {
  opacity: 0.55;
  transition-delay: 0.15s;
}

/* The quiz's level strip: a hairline bar rather than Bootstrap's 16px one, on a
   track that reads against the gradient. */
.guide-quiz-progress {
  --bs-progress-height: 0.25rem;
  --bs-progress-bg: rgba(var(--bs-dark-rgb), 0.1);
}

/* "Новый уровень: …!" lands the way the tick does. */
.guide-quiz-level-up {
  animation: quizMarkIn 0.45s var(--ease-plop) both;
  transform-origin: left center;
}

@media (prefers-reduced-motion: reduce) {
  .guide-quiz-mark,
  .guide-quiz-level-up { animation: none; }
  .guide-quiz-option { transition-duration: 0.15s; }
  .guide-quiz-option.is-other { transition-delay: 0s; }
}

.guide-quiz-card {
  width: 72px;
  height: auto;
}


/* The heart on a reading (partials/like-button.php — Bootstrap utilities otherwise)
   sits high three ways at once: Bootstrap Icons' heart is drawn above the middle of
   its own em box (y 1–13.8 of 16), the .btn text trim centres on cap height, which
   the icon overshoots upwards, and a heart's weight is all in its two lobes. Nudged
   down by position, not vertical-align, so the line box — and the button — keep
   their size and the label beside it doesn't move. */
[data-like-icon] {
  position: relative;
  top: 0.09em;
}

/* The cookie notice (partials/cookie-banner.php): fixed to the screen's foot, it
   rises in through the bottom edge and lands on a spring, and once answered sinks
   back past that edge — the way a floating block pinned there lives below it,
   peeks out and hides back. Fixed rather than sticky so it can travel past the
   edge without making the page longer: no scrollbar flashes. `display` transitions
   with allow-discrete, so once hidden it stays drawn until its exit has played;
   browsers without that just hide it at once.
   Until answered it rings for attention now and then — "ding" — and five still
   seconds. Rare and short on purpose: a banner that never stops moving gets in the
   way of reading. */
[data-cookie-banner]:not([hidden]) {
  animation: cookieArrive 0.8s linear both, cookieFadeIn 0.2s ease-out both;
}

[data-cookie-banner][hidden] {
  animation: cookieAway 0.45s both;
  transition: display 0.45s allow-discrete;
}

[data-cookie-banner]:not([hidden]) > .container {
  animation: cookieDing 6s linear 3s infinite;
}

/* Its shadow-lg is cast upwards: Bootstrap's falls 1rem down, which on a banner
   pinned to the screen's foot goes off-screen, leaving its top edge — the one that
   meets the page — with hardly any. */
[data-cookie-banner] > .container {
  --bs-box-shadow-lg: 0 -0.5rem 2rem rgba(0, 0, 0, 0.16), 0 0 0.5rem rgba(0, 0, 0, 0.06);
  /* Frosted glass: the page shows faintly through the bg-white, blurred. 0.9 sits
     between Bootstrap's bg-opacity-75 (too see-through) and 100; no utility for
     that, nor for backdrop-filter. */
  --bs-bg-opacity: 0.9;
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}

/* Arriving: from just past its own height below, a damped spring that overshoots
   10% of its height upwards and settles — sampled from
   y = y0·e^(−ζωt)·(cos ω_d t + ζ/√(1−ζ²)·sin ω_d t), ζ = 0.6, so it lands rather
   than bounces. Percentages, so a taller banner (a phone's) travels further. */
@keyframes cookieArrive {
  0%   { transform: translateY(110%); }
  5%   { transform: translateY(101.4%); }
  10%  { transform: translateY(81.8%); }
  15%  { transform: translateY(58.4%); }
  20%  { transform: translateY(36%); }
  25%  { transform: translateY(17.6%); }
  30%  { transform: translateY(4.1%); }
  35%  { transform: translateY(-4.6%); }
  40%  { transform: translateY(-9%); }
  45%  { transform: translateY(-10.4%); }
  50%  { transform: translateY(-9.7%); }
  55%  { transform: translateY(-7.9%); }
  60%  { transform: translateY(-5.7%); }
  65%  { transform: translateY(-3.6%); }
  70%  { transform: translateY(-1.8%); }
  75%  { transform: translateY(-0.5%); }
  80%  { transform: translateY(0.4%); }
  85%  { transform: translateY(0.8%); }
  90%  { transform: translateY(1%); }
  95%  { transform: translateY(0.9%); }
  100% { transform: none; }
}

@keyframes cookieFadeIn { from { opacity: 0; } }

/* Going: a wind-up first — a small lift the other way, gathering itself — then
   down past the edge the way it came, speeding up, fading as it goes. */
@keyframes cookieAway {
  0%   { transform: none;              opacity: 1; animation-timing-function: ease-in-out; }
  30%  { transform: translateY(-6px);  opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translateY(110%);  opacity: 0; }
}

/* The ding: a soft damped spring — a kick up to 8px, then smooth swings round its
   place, each ~0.44 of the one before (8 → 3.6 below → 1.6 → 0.7) — in the first
   0.9s (15%) of a 6s cycle. Sampled from y = −e^(−ζωt)·sin(ω_d t), ζ = 0.25, 1.9
   swings; close enough samples that linear steps read as one curve. */
@keyframes cookieDing {
  0%     { transform: none; }
  0.62%  { transform: translateY(-4.95px); }
  1.25%  { transform: translateY(-7.65px); }
  1.88%  { transform: translateY(-8px); }
  2.5%   { transform: translateY(-6.45px); }
  3.12%  { transform: translateY(-3.78px); }
  3.75%  { transform: translateY(-0.85px); }
  4.38%  { transform: translateY(1.6px); }
  5%     { transform: translateY(3.14px); }
  5.62%  { transform: translateY(3.61px); }
  6.25%  { transform: translateY(3.15px); }
  6.88%  { transform: translateY(2.08px); }
  7.5%   { transform: translateY(0.78px); }
  8.12%  { transform: translateY(-0.4px); }
  8.75%  { transform: translateY(-1.23px); }
  9.38%  { transform: translateY(-1.59px); }
  10%    { transform: translateY(-1.5px); }
  10.62% { transform: translateY(-1.1px); }
  11.25% { transform: translateY(-0.53px); }
  11.88% { transform: none; }
  12.5%  { transform: translateY(0.45px); }
  13.12% { transform: translateY(0.68px); }
  13.75% { transform: translateY(0.7px); }
  14.38% { transform: translateY(0.56px); }
  15%, 100% { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  [data-cookie-banner],
  [data-cookie-banner][hidden],
  [data-cookie-banner]:not([hidden]) > .container { transition: none; animation: none; }
}

/* It lands the way the quiz's tick does when liked; initLikes() sets data-like-pop. */
[data-like-pop] [data-like-icon] {
  display: inline-block;
  animation: quizMarkIn 0.45s var(--ease-plop) both;
}

@media (prefers-reduced-motion: reduce) {
  [data-like-pop] [data-like-icon] { animation: none; }
}


/* Items held back for "Показать ещё" (initShowMore() in app.js). Hidden only when
   JS runs — the .js class is set in <head> before paint — so a page without the
   script shows the whole list instead of losing the rest. */
.js [data-more] {
  display: none;
}

/* EXPERIMENT: block headers (bg-light + shadow-lg) as a left-to-right gradient,
   a darker grey fading into the usual light. Delete this rule to go back. */
/* .bg-light.shadow-lg {
  background-image: linear-gradient(to top, #eceef1, var(--bs-light) 45%);
} */

/* ─── Utilities Bootstrap doesn't ship ───────────────────────────────────────── */

/* Responsive border (Bootstrap has none): a left border from md up. */
@media (min-width: 768px) {
  .border-start-md { border-left: 1px solid var(--bs-border-color); }
}

/* Bootstrap ships row-cols only 1–6; extend to 8 for dense thumbnail strips
   (guide-card "explore all" + the guide-promo block both intend 8 per row at lg). */
@media (min-width: 992px) {
  .row-cols-lg-8 > * { flex: 0 0 auto; width: 12.5%; }
}
