/* ==========================================================================
   Astrocaffe — block styles, Phase 1a Stage 3
   Rebuilt from docs/02-design-system.md's extracted system, using the
   tokens in design/tokens.css. Not copied file-by-file from
   docs/_design-pack/css/style.css — structure follows the pack (same
   selectors, same layout behaviour) but every literal colour, size, radius,
   shadow and weight has been routed through a token where one exists, per
   the consolidation decided 2026-08-03.

   No Rails, no locale keys yet — this is Phase 1a. Copy is plain text,
   reused from the pack's Slovenian, not yet translated or moved to YAML.
   ========================================================================== */
/* ---- Reset / base — shared by every block page ---- */
* {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    background-color: var(--c-blue-950);
    font-family: var(--font-body);
    color: var(--c-white-85);
    font-size: var(--fs-base);
    line-height: var(--lh-loose);
}

img {
    max-width: 100%;
    display: block;
    border: 0;

    /* `height: auto` is the other half of `max-width: 100%` and was missing.
       Added 2026-08-06 with the width/height attributes that now sit on the
       markup for layout stability: an `height="2100"` attribute is a
       presentational hint that sets a real CSS height, so without this the
       constrained width no longer scales the height with it — a 2100px
       square rendered 2100px tall in a 351px slot, and it also silenced
       `aspect-ratio` wherever that is used to art-direct a crop. */
    height: auto;
}

a {
    color: var(--c-orange-400);
    text-decoration: none;
    transition: color var(--transition);
}

a:hover {
    color: var(--c-green);
}

ul {
    margin: 0;
    padding: 0;
    list-style: none;
}

p {
    margin: 0 0 var(--space-3);
}

/* HEADINGS ARE LORA. Decided 2026-08-07 (Zlatan): every heading is
   --font-heading, and --font-body on a heading is a special occasion, set
   by hand, never a default.

   Before this, `h2` alone declared --font-body and h3/h4 inherited Source
   Sans from the body. That put the typeface on the tag, so correcting a
   heading LEVEL — as the 8-h1 SEO fix on product.html did — silently changed
   the typeface of six section headings from serif to sans. Heading level
   belongs to the document; heading look belongs to the class. */
h1,
h2,
h3,
h4 {
    margin: 0 0 var(--space-2);
    font-family: var(--font-heading);
    font-weight: var(--fw-light);
    line-height: var(--lh-tight);
}

h1 {
    font-size: var(--fs-5xl);
    font-weight: var(--fw-bold);
    letter-spacing: -1px;
}

h2 {
    font-size: var(--fs-4xl);
    font-weight: var(--fw-light);
}

.w-container {
    max-width: var(--container-w);
    margin: 0 auto;
    padding-left: .75em;
    padding-right: .75em;
}

/* Two halves of ONE .w-container, with NO wrapper (2026-10-03, Zlatan's
   drawing: "I don't want to put it in a w-container"). Two .w-container-50
   side by side, straight in a section:

       <section class="bgr-blue-900">
           <div class="w-container-50">left</div>
           <div class="w-container-50">right</div>
       </section>

   Together they are exactly as wide as a .w-container (940px at most, the
   screen's width below that), centred; each is half of it and brings the
   container's padding on BOTH its sides, so the outer edges sit where a
   container's padding does and the two paddings meet in the middle.

   How, without a wrapper: they float left, each min(50%, half the container)
   of the PARENT; the first of a pair gets the margin that centres the pair
   (a percentage margin is a share of the parent's width); and the parent,
   whatever it is, becomes display: flow-root so it still wraps its floats and
   its background still shows. A lone half is centred. On a phone they stack,
   each the whole width. Needs :has (every current browser). */
section:has(> .w-container-50),
div:has(> .w-container-50) {
    display: flow-root;
}

.w-container-50 {
    float: left;
    width: min(50%, calc(var(--container-w) / 2));
    padding-left: .75em;
    padding-right: .75em;
}

.w-container-50:not(.w-container-50 + .w-container-50) {
    margin-right: calc((100% - min(100%, var(--container-w))) / 2);
}

.w-container-50:only-child {
    margin-left: calc((100% - min(50%, calc(var(--container-w) / 2))) / 2);
}

@media (max-width: 767px) {
    .w-container-50,
    .w-container-50:not(.w-container-50 + .w-container-50),
    .w-container-50:only-child {
        float: none;
        width: 100%;
        margin-right: 0;

        /* margin-bottom: var(--space-12) */
    }
}

.narrow-box {
    max-width: 800px;
    margin-left: auto;
    margin-right: auto;
}

.text-center {
    text-align: center !important;
}

.uppercase {
    text-transform: uppercase !important;
}

.hidden {
    display: none;
}

.button-row {
    text-align: center;
}

.mb-0 {
    margin-bottom: 0 !important;
}

.mb-1 {
    margin-bottom: var(--space-4) !important;
}

.mb-1x {
    margin-bottom: var(--space-6) !important;
}

.mb-2 {
    margin-bottom: var(--space-8) !important;
}

.mb-3 {
    margin-bottom: var(--space-12) !important;
}

.mb-4 {
    margin-bottom: var(--space-16);
}

/* The mb- steps, sideways (Zlatan, 2026-10-07). */
.mr-1 {
    margin-right: var(--space-4) !important;
}

.mb-5 {
    margin-bottom: var(--space-20);
}

.padding-x {
    padding-top: var(--space-8);
    padding-bottom: var(--space-8);
}

/* ==========================================================================
   Mobile: full-width sections centre their text
   Added 2026-09-08 (Zlatan, from the mobile review).

   `.reading-intro` already did this and it was the reason he asked for it
   everywhere — a single column at 375px reads better centred, because the
   ragged right edge of left-aligned text is the only thing distinguishing one
   block from the next once everything is the same width.

   Stated once here rather than per section, so a new section inherits it
   instead of being forgotten.

   THE OPT-OUTS ARE THE INTERESTING PART. Three kinds of content get worse
   centred, and all three are opted out below rather than left to look wrong:

     - anything with a bullet or an icon hanging to the LEFT of it. The bullet
       stays put while the text slides away from it, so the two come apart.
     - form fields. A label centred over a full-width input has no visible
       relationship to it, and the eye has to re-find the start of every line.
     - cards. They are their own little columns; centring is decided inside
       the card, not by the section it happens to sit in.
     - a long text meant to be READ, not scanned — the astrology basics
       chapter (added 2026-09-26, PROVISIONAL). The rule above is right for
       blocks of two or three sentences; over twenty paragraphs a centred
       column makes the eye hunt for the start of every line.
   ========================================================================== */
@media (max-width: 767px) {
    article > section {
        text-align: center;
    }

    .form-wrapper,
    .order-features,
    .author-facts,
    .feature-list,
    .review-card,
    .confirm-list,
    .place-suggestions,
    .account-menu,
    .chapter-layout,
    .chapter-card,
    .planet-list {
        text-align: left;
    }
}

.padding-xl {
    padding-top: var(--space-12);
    padding-bottom: var(--space-12);
}

.padding-xxl {
    padding-top: var(--space-16);
    padding-bottom: var(--space-16);
}

/* Demo-page-only chrome — not part of the design system, just lets each
   block file stand alone with some breathing room and a caption. */
.demo-section {
    padding: var(--space-8) var(--space-4);
}

.demo-caption {
    max-width: var(--container-w);
    margin: 0 auto var(--space-4);
    font-family: 'SFMono-Regular', Consolas, Menlo, monospace;
    font-size: var(--fs-xs);
    color: var(--c-text-soft);
}

.demo-caption a {
    color: var(--c-blue-500);
}

.demo-section.dark-demo {
    background: var(--c-blue-950);
}

.demo-section.dark-demo .demo-caption {
    color: var(--c-white-75);
}

/* ==========================================================================
   Buttons / CTAs — _cta_button
   ========================================================================== */
.button {
    display: inline-block;

    /* Vertical padding one step down from horizontal on the spacing scale.
       1rem vs 1.5rem is 33% smaller — the scale has no 1.125rem step, which
       is what an exact 25% would need. */
    padding: var(--space-4) var(--space-6);
    border-radius: var(--radius);
    background-color: var(--c-orange-red);
    font-family: var(--font-body);
    color: var(--c-white-soft);
    font-weight: var(--fw-regular);
    text-align: center;
    letter-spacing: 1px;
    text-transform: uppercase;
    cursor: pointer;
    border: none;
    transition: background-color var(--transition), transform var(--transition);
    font-size: var(--fs-md);
}

.button:hover {
    background-color: var(--c-green);
    color: var(--c-white);
    box-shadow: var(--shadow-tight);
}

.button.outline {
    /* padding now inherited from .button — was a duplicate of the same values */
    border: 2px solid var(--c-white-75);
    background-color: transparent;
    color: var(--c-white-75);
    text-transform: none;
    letter-spacing: normal;
}

.button.outline:hover {
    background-color: var(--c-green);
    color: var(--c-white);
    border-color: var(--c-green);
}

.button.outline.blue {
    box-shadow: none;
    border-color: var(--c-border);
    color: var(--c-text);
}

.button.outline.blue:hover {
    background-color: var(--c-green);
    color: var(--c-white);
    border-color: var(--c-green);
}

.button.outline.small-button {
    padding: var(--space-2) var(--space-4);
}

.cart-button {
    margin: var(--space-6) 0;
    padding: var(--space-4) 2rem var(--space-4) var(--space-20);
    background-color: var(--c-orange-red);
    background-image: url(../img/i-cart.svg);
    background-position: var(--space-6) 47%;
    background-size: 2.5rem;
    background-repeat: no-repeat;
    box-shadow: var(--shadow-tight);
    font-size: var(--fs-lg);
    letter-spacing: 1px;
    font-weight: var(--fw-semibold);
}

@media (max-width:767px) {
    #naroci-cols2 .cart-button {
        margin: var(--space-1) 0;
    }
}

.cart-button:hover {
    background-color: var(--c-green);
    background-image: url(../img/next-icon.png);
    background-size: 32px;
    background-position: 30px 50%;
}

/* ==========================================================================
   Site chrome — _header, _nav_cart_icon, _footer, _floating_cta
   ========================================================================== */
.site-header {
    background-color: var(--c-header);
    width: 100%;
}

.site-header:hover {
    background-color: var(--c-blue-850);
}

/* Stripped chrome — the reduced header/footer used on checkout steps.

   It keeps `.site-header`'s own background, so it is the same bar as
   everywhere else; only the contents are stripped back. That background is
   semi-transparent by design, so the page nests this header INSIDE its first
   starfield section exactly as `product.html` nests it inside the hero —
   given something dark behind it, the shared token reads correctly and no
   colour override is needed.

   The hover darkening is dropped: with no nav links to reveal it only reads
   as a stray hit area. */
.stripped-header:hover {
    background-color: var(--c-header);
}

.nav-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 0 4%;
    margin: 0;
    position: relative;
}

.nav-logo {
    padding: var(--space-2) 0;
    flex-grow: 1;
}

.nav-logo .logo {
    height: 40px;
    width: auto;
}

.nav-menu {
    display: flex;
    align-items: stretch;
}

.nav-link {
    display: inline-block;
    padding: var(--space-4);
    background-color: transparent;
    font-family: var(--font-body);
    color: var(--c-white-75);
    font-weight: var(--fw-regular);
}

.nav-link:hover {
    background-color: var(--c-green);
    color: hsla(0, 0%, 100%, .9);
}

.nav-link.active-link {
    border-top: 4px solid var(--c-orange);
    color: var(--c-orange);
    padding-top: .7rem;
}

.nav-link.active-link:hover {
    background-color: var(--c-header);
}

.menu-toggle {
    display: none;
    flex-direction: column;
    gap: 6px;
    cursor: pointer;
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    transition: var(--transition);
}

.menu-toggle:hover {
    background-color: var(--c-blue-950-80);
}

.menu-toggle span {
    display: block;
    width: 22px;
    height: 2px;
    background: var(--c-white);
}

/* 899, not 900. The hero rules below start at `min-width: 900px`, and a
   max-width of 900 would match the same pixel — at exactly 900 the drawer
   opened over a desktop-laid-out hero. Written the way the 768 boundary
   already is. Measured and fixed 2026-09-13. */
@media (max-width: 899px) {
    .nav-menu {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        flex-direction: column;
        background: var(--c-blue-950);
        z-index: 100;
    }

    .nav-menu.open {
        display: flex;
    }

    .nav-menu .nav-link {
        width: 100%;
        text-align: left;
        font-size: 1.25em;
    }

    .nav-link.active-link {
        border-top: none;
        border-left: 4px solid var(--c-orange);
        padding-top: var(--space-4);
    }

    .menu-toggle {
        display: flex;
    }
}

/* ---- _nav_account — "My astro", signed out and signed in ----
   Added 2026-09-08.

   ONE ITEM, TWO STATES, set by data-account-state on the wrapper:

     out  the link opens _auth_overlay. No menu.
     in   the link opens this dropdown.

   The state is an attribute rather than two different markups because the
   only thing that changes is what the click does — the item, its position and
   its label are identical either way, and a signed-in header that reflows is
   a header that looks broken for a moment on every page load.

   In Rails the <ul> is rendered ONLY when signed in. It is present in both
   states here so the static design pages can show the menu at all; the CSS
   below is what hides it when signed out, and it is belt-and-braces, not the
   mechanism. */
.nav-account {
    position: relative;
    display: flex;
    align-items: stretch;
}

.account-menu {
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    z-index: 10;
    min-width: 180px;

    /* An address can be long. The menu grows to fit one, then stops, and the
       row inside it truncates rather than the menu reaching off the screen.
       The viewport term is what keeps it on a narrow phone held sideways. */
    max-width: min(22rem, calc(100vw - var(--space-8)));
    padding: var(--space-2) 0;
    border-radius: var(--radius-sm);
    background-color: var(--c-blue-950);
    box-shadow: var(--shadow-tight);
}

.nav-account[data-account-state="in"].open .account-menu {
    display: block;
}

/* Signed in, the trigger goes to full white. The links either side sit at 75%,
   so the difference is small on purpose: it is a confirmation for somebody who
   already knows, not an announcement. Zlatan, 2026-09-12.

   It is NOT orange, which was the first idea. Orange is already this nav's
   "you are on this page" (.active-link below), and the pages behind this very
   menu — Charts, Orders, Addresses, Settings — would have carried both meanings
   on the same item at the same time. */
.nav-account[data-account-state="in"] > .nav-link {
    color: var(--c-white);
}

.nav-account[data-account-state="in"] > .nav-link:hover {
    color: var(--c-white);
}

/* ---- the signed-in address, at the top of the menu ----
   Added 2026-09-12. The only thing we can honestly call the user: sign-up takes
   an email and a password, nothing else, and the name on a chart belongs to the
   chart — it is often a gift for somebody else.

   Not a link, and not in the header. It would sit on screen in every screenshot
   and every shared screen, and it is the identifier half of a credential. It
   also answers a different question from "My astro": which account, rather than
   where does this go. */
.account-email {
    display: flex;
    padding: var(--space-1) var(--space-6) var(--space-3);
    margin-bottom: var(--space-2);
    border-bottom: 1px solid var(--c-blue-800);
    font-size: var(--fs-xs);
    color: var(--c-white-55);
    cursor: default;
}

/* TRUNCATE THE NAME, NEVER THE DOMAIN. `text-overflow: ellipsis` cuts the end
   off, which would throw away the half that identifies the account: you know
   your own address by its domain and its first few characters, not by its last
   ones. So the two halves are separate elements — the local part shrinks and
   the domain does not — and "zlatanbogd…@gmail.com" falls out of flexbox with
   no script at all.

   In Rails that is one `split('@')`. Worth it. */
.account-email__local {
    overflow: clip;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 1.5ch;

    /* always show who, even at the worst width */
}

/* Last resort only: a domain long enough to fill the menu on its own truncates
   too, rather than pushing out of the box. The local part has already shrunk to
   its minimum by then. */
.account-email__domain {
    flex: 0 0 auto;
    max-width: 100%;
    overflow: clip;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.account-link {
    display: block;
    padding: var(--space-2) var(--space-6);
    font-family: var(--font-heading);
    color: var(--c-white-75);
    white-space: nowrap;
}

.account-link:hover {
    background-color: var(--c-blue-900);
    color: var(--c-white);
}

/* Log out is the one destructive item in the list, so it is separated rather
   than coloured — a red item in a six-item menu reads as an error state. */
.account-link--out {
    margin-top: var(--space-2);
    padding-top: var(--space-3);
    border-top: 1px solid var(--c-blue-800);
}

/* 899 to match the drawer above — the two have to switch on the same pixel or
   the account menu styles itself for a drawer that is not there. */
@media (max-width: 899px) {
    /* In the drawer there is nothing to drop DOWN from — the nav is already a
       vertical list, so the submenu indents inside it instead of floating. */
    .nav-account {
        flex-direction: column;
    }

    .account-menu {
        position: static;
        min-width: 0;
        padding: 0 0 var(--space-2) var(--space-6);
        border-radius: 0;
        background-color: transparent;
        box-shadow: none;
    }

    .account-link {
        padding: var(--space-2) var(--space-4);
        font-size: 1.1em;
    }
}

.cart-icon {
    padding: 5px;
    border-radius: var(--radius-sm);
    transition: var(--transition);
    position: relative;
    width: 34px;
    margin-left: var(--space-1);
}

.cart-icon:hover {
    background-color: var(--c-green);
}

.cart-badge {
    background-color: var(--c-orange-400);
    color: var(--c-white);
    position: absolute;
    font-size: var(--fs-xs);
    line-height: 1;
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    right: -5px;
    top: -5px;
}

.footer {
    padding: var(--space-12) var(--space-4) 1px;
    background-color: var(--c-blue-950);
    background-image: linear-gradient(180deg, var(--c-blue-950), var(--c-blue-900));
    color: var(--c-white-75);
    border-top: 7px solid var(--c-blue-700);

    /* border-top: var(--border-orange-5) */
}

.footer .w-container {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    justify-content: space-between;
    margin-bottom: var(--space-12);
}

.footer-company {
    /* font-family: var(--font-heading) */
    color: var(--c-cream);
    font-size: var(--fs-base);
}

.footer-heading {
    color: var(--c-orange);
    font-weight: var(--fw-regular);
    letter-spacing: 1px;
    margin-bottom: var(--space-1);
    line-height: var(--fs-2xl);
}

.footer-company .footer-heading {
    margin-top: 1.5rem;
}

.stripped-footer .footer-company .footer-heading {
    margin-top: 1.45rem;
}

.company-address {
    /* margin-bottom: 1.6rem */
    /* font-weight: var()  <- left without a token (2026-09-29); an empty var() is invalid, so browsers skipped it anyway */
}

.footer-nav {
    padding-left: var(--space-4);
    border-left: 1px solid hsla(0, 0%, 100%, .25);

    /* font-family: var(--font-heading) */
    color: var(--c-cream);
    font-size: var(--fs-base);
}

.footer-nav a,
.footer-company a {
    display: block;
    line-height: var(--fs-2xl);
}

.footer-nav a {
    line-height: var(--fs-3xl);
}

.whitelink {
    color: var(--c-white-75);
    line-height: 24px;
}

.whitelink:hover {
    color: var(--c-green);
}

/* Payment, modernised 2026-09-16 (Zlatan: "This is old school. Make it
   modern."). It sat in a near-white box on a dark footer with a strip of
   2010-era 3-D Secure logos. Now it is a column like its neighbours: the same
   orange heading as "Published by", a row of quiet outlined badges, and the
   sentence underneath with a lock. The divider-grade border matches
   .footer-nav's own column rule, so the three columns read as one set. */
.footer-payment {
    max-width: 45%;
}

.payment-methods {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: var(--space-2) 0 var(--space-3);
    padding: 0;
    list-style: none;
}

.payment-methods li {
    padding: var(--space-1) var(--space-2);
    border: 1px solid hsla(0, 0%, 100%, .25);
    border-radius: var(--radius-sm);
    color: var(--c-white-75);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: .02em;
    line-height: 1;
    white-space: nowrap;
}

/* --c-white-55 over the footer's gradient measures 6.16:1 at its dark end and
   5.65:1 at its light end, so the quietest text in the column still passes. */
.payment-text {
    padding-left: var(--space-6);
    background: url('../img/icons/lock-white.svg') no-repeat 0 .2em / .95rem;
    font-size: var(--fs-sm);
    line-height: var(--lh-relaxed);
    color: var(--c-white-55);
}

/* THE PAYMENT COLUMN READS AS ITS OWN PANEL. Zlatan, 2026-09-24: "make it
   visible that this is a different set of information." Beside two columns of
   links, a third column of text looked like more links. A quiet inset box with
   a rule around it separates it without adding colour to the footer.

   The methods are grouped by kind behind one icon each — card, and the phone
   wallets — rather than five word badges in a row. The icons are plain
   glyphs, NOT brand marks: the real Visa and Mastercard artwork comes from
   Stripe's asset kit in Phase 2, and a hand-drawn logo is worse than none.
   Scoped to .footer-payment so the checkout's .payment-methods--light keeps
   its badges, where they sit beside a radio and have to read as a set. */
.footer-payment {
    padding: var(--space-4);
    border: 1px solid var(--c-white-25);
    border-radius: var(--radius);
    background-color: var(--c-blue-950-80);
}

.footer-payment .payment-methods {
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

.footer-payment .payment-methods li {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0;
    border: 0;
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    letter-spacing: normal;
    color: var(--c-cream);
}

.footer-payment .payment-methods img {
    flex: 0 0 auto;
    opacity: .7;
}

.legal-footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-2);
    margin-top: var(--space-8);

    /* font-family: var(--font-heading) */
    color: var(--c-cream);
    font-size: .75rem;
}

.legal-footer a {
    /* .l-white merged into .whitelink 2026-08-03 — only the subfooter-specific
       layout (inline flow + gap) needed to survive; colour now comes from
       .whitelink itself. */
    display: inline-block;
    margin-right: var(--space-4);

    /* Capitals from here, not typed (2026-10-09): typed capitals made
       "PRIVACY" a second row in the text file beside "Privacy". */
    text-transform: uppercase;
}

/* Three columns in one row from 768px. Fixed 2026-09-21 (Zlatan: broken
   between 768 and 817px). The payment column used to ask for its full 45% up
   front, and company + sitemap + 45% + the gaps is wider than the container
   until about 818px, so it dropped to a second row. Now it starts from
   nothing and takes whatever the other two leave, still capped at 45%; 12rem
   is the narrowest its badges and sentence stay readable in. */
@media (min-width: 768px) {
    .footer-payment {
        flex: 1 1 0;
        min-width: 12rem;
    }
}

@media (max-width: 767px) {
    .footer .w-container {
        justify-content: flex-start;
        flex-shrink: 1;
    }

    .footer-nav {
        width: 50%;
        flex-grow: 1;
        margin-bottom: var(--space-4);
    }

    .footer-payment {
        max-width: 100%;
    }
}

/* The cookie consent banner was here. Removed 2026-09-06 at Zlatan's
   request — the whole block, its markup on every page, initCookieBanner()
   and blocks/cookie-banner.html. See docs/07-removals.md. The cookie POLICY
   is untouched: it is page 17 on the page list and still linked from every
   footer. */
/* Floating CTA — no markup in the pack either (#create-book-cta target
   doesn't exist in product.html); rebuilt from the .floating-cta rules,
   which do exist in the CSS. */
/* BUILT LIKE THE CART'S CHECKOUT BAR (Zlatan, 2026-09-24). It was a rounded
   pill floating at the right edge; the cart's bar is a full-width strip across
   the foot of the screen, blurred, with a drawn top seam. Two bars doing the
   same job — "the way on is down here" — now look like one idea.

   The collapse tab survives the change: it still slides the whole strip right
   by its own width less the tab, so the tab stays at the edge and nothing is
   dismissed for good. */
.floating-cta {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 200;
    padding: var(--space-3) var(--space-4) var(--space-3) 0;
    border-top: 1px solid var(--c-blue-700);
    background-color: var(--c-blue-950-80);
    backdrop-filter: blur(6px);
    box-shadow: var(--shadow-tight);
    text-align: center;
    --cta-tab: 3.5rem;
    transition: transform .35s ease;
}

/* The button keeps the right-hand end, as the checkout bar's does, and the
   price sits between them rather than leaving the middle empty. */
.floating-cta.visible {
    justify-content: space-between;
}

.floating-cta__price {
    margin: 0 auto 0 0;
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    color: var(--c-orange);
    font-variant-numeric: tabular-nums;
}

/* "Free shipping" beside a lone book price — Zlatan, 2026-09-27. The rule:
   every book price at a buy point, never on a card, never where free shipping
   is already said nearby (home's and the product hero's price boxes), never
   on an ebook. Small and quiet: it answers a doubt, it does not sell. */
.price-note {
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    color: var(--c-text-soft);
}

/* In the order overlay the note always shares the price's line (Zlatan,
   2026-09-27). In the floating bar too — except on a narrow phone, where the
   bar gives the price about 115px and one line would squeeze the button.
   539/540: the product-card boundary, a special case listed in tokens.css. */
.floating-cta__price .price-note {
    font-size: var(--fs-xs);
    white-space: nowrap;

    /* "Free / shipping" on two lines squeezed the price */
    color: var(--c-white-75);
}

/* The bar on a narrow phone, in one place: the price aligns left on a 2rem
   line (Zlatan, 2026-09-27) and "Free shipping" drops beneath it. */
@media (max-width: 539px) {
    .floating-cta__price {
        text-align: left;
        line-height: 2rem;
    }

    .floating-cta__price .price-note {
        display: block;
        line-height: 1.4;

        /* its own, not the price's: no gap under it */
    }

    /* No room for the struck price here: it took a line of its own and
       pushed "Order the book" onto two (measured at 375px, 2026-10-08). The
       paid price stays; the comparison is on the page, twice. */
    .floating-cta__price .price-was {
        display: none;
    }
}

.floating-cta.visible {
    display: flex;
    align-items: center;
}

/* THE TAB (Zlatan, 2026-09-21: the close button became an arrow). Collapsing
   slides the whole bar right by its width less the tab's, so the tab alone
   stays at the edge, and the arrow turns to point back in. Both are one
   transform on existing boxes: nothing re-lays out and nothing is removed,
   so the bar comes back exactly as it left. --cta-tab is the tab's width,
   shared by the tab and the slide so the two can never disagree. */
.floating-cta[data-cta-state="collapsed"] {
    transform: translateX(calc(100% - var(--cta-tab)));
}

.floating-cta__toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    align-self: stretch;
    flex: 0 0 var(--cta-tab);
    margin: calc(-1 * var(--space-4)) var(--space-4) calc(-1 * var(--space-4)) 0;
    padding: 0;
    border: 0;
    border-right: 1px solid hsla(0, 0%, 100%, .15);
    background: transparent;
    cursor: pointer;
    opacity: .75;
    transition: opacity var(--transition);
}

.floating-cta__toggle:hover,
.floating-cta__toggle:focus-visible {
    opacity: 1;
}

.floating-cta .floating-cta__toggle img {
    height: 1.75rem;
    width: auto;
    margin: 0;
    transition: transform .35s ease;
}

.floating-cta[data-cta-state="collapsed"] .floating-cta__toggle img {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .floating-cta,
    .floating-cta .floating-cta__toggle img {
        transition: none;
    }
}

.floating-cta .close-button {
    position: absolute;
    top: .5rem;
    right: .75rem;
    opacity: .6;
    cursor: pointer;
    border: 1px solid white;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background-image: url('../img/icons/close-white.svg');
    background-position: center center;
    background-size: 1rem;
}

.floating-cta .close-button:hover {
    opacity: 1;
}

.floating-cta .button {
    margin: 0;
}

@media (max-width: 600px) {
    /* .floating-cta {
        ||*| right: 0 |*||
        /* left: 0 */
    /* bottom: 1rem */
    /* min-width: 0 */
    /* border-radius: 0 */
}

/* ==========================================================================
   Section framing — _section_kicker, _arrow_divider, _curve_divider
   ========================================================================== */
.section-label {
    font-weight: var(--fw-bold);
    color: var(--c-orange);
    margin-bottom: var(--space-6);
    font-family: var(--font-body);
}

.section-label span {
    border-bottom: 1px dotted var(--c-orange-50);
    padding-bottom: .15rem;
    letter-spacing: 2px;
    padding-left: 0.5rem;
    padding-right: 0.5rem;
}

.section-arrow {
    display: block;
    width: 0;
    height: 0;
    border-left: 20px solid transparent;
    border-right: 20px solid transparent;
    border-top: 20px solid var(--c-text);
    margin-left: auto;
    margin-right: auto;
}

.section-arrow.dark {
    border-top-color: #010506;
}

.section-arrow.orange {
    border-top-color: var(--c-orange);
}

/* Was .yellow until 2026-10-08 (Zlatan): the colour is --c-cream, and the
   name now says so. */
.section-arrow.cream {
    border-top-color: var(--c-cream);
}

.section-arrow.tan {
    border-top-color: var(--c-tan);
}

.section-arrow.white {
    border-top-color: var(--c-white);
}

.section-arrow.grey {
    border-top-color: var(--c-tan);
}

/* .section-arrow.star removed 2026-08-03 — --c-blue-star merged into --c-blue-950,
   which made .star an exact duplicate of .dark. Use .dark. */
.section-arrow.blue950 {
    border-top-color: var(--c-blue-950);
}

.section-arrow.blue900 {
    border-top-color: var(--c-blue-900);
}

.section-arrow.blue800 {
    border-top-color: var(--c-blue-800);
}

.section-arrow.blue850 {
    border-top-color: var(--c-blue-850);
}

.section-arrow.blue700 {
    border-top-color: var(--c-blue-700);
}

.section-arrow.blue500 {
    border-top-color: var(--c-blue-500);
}

.curve-divider,
.curve-divider img {
    width: 100%;
    min-width: 100%;
    max-width: 100%;
    padding: 0;
    margin: 0;
}

/* ==========================================================================
   Content sections
   ========================================================================== */
/* _hero, _scroll_cue

   THE HERO PHOTOGRAPH IS AN <img>, NOT A BACKGROUND. Decided 2026-08-04,
   reasoning in docs/05-decisions.md.

   It is the Largest Contentful Paint element on every page, and a CSS
   background is invisible to the browser's preload scanner — it cannot begin
   downloading until the stylesheet has been fetched, parsed and the selector
   matched. An <img> in the markup starts immediately, accepts `srcset`, and
   lets each page supply its own photograph without anyone editing CSS.

   The image is absolutely positioned, so it is out of flow and the section's
   height still comes from its contents exactly as it did as a background. */
.product-intro {
    position: relative;
    min-height: 400px;
    background-color: var(--c-blue-950);

    /* border-bottom: var(--border-blue-7) */
}

/* <picture> is only a wrapper for the art-directed sources and must not
   generate a box of its own — its own content is out of flow, but an inline
   box with nothing in it still forces a line box on the parent, which would
   add a few stray pixels to the hero's height. */
.product-intro picture {
    display: contents;
}

.hero-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;

    /* The crop anchor switches at the SAME breakpoint as the art itself, or
       the two fight each other.

       Anchored right — correct on desktop, where it keeps the woman in
       frame — a narrow slot would discard the book entirely and leave a face
       with no product in it.

       35% is measured, not eyeballed. The mobile file is 698x964 (portrait,
       recomposed 2026-08-05 — it was 1000x929 and lost 63% of its width to
       the crop; now it loses 45%). At a 375-wide slot the visible window is
       source x 109-495, which keeps the printed name (~90-280) intact and
       still reaches the smile. The name is the entire point of the product,
       so it is the thing that must never be cropped out. */
    object-position: right center;
}

@media (min-width: 900px) {
    .hero-bg {
        object-position: right center;
    }
}

/* The homepage hero breathes. Asked for 2026-09-12: gentle, slow.

   ID-scoped, not a class: only the homepage wants it today. It becomes a
   grouped selector the first time a second page does, and the ID stays.

   A SLOW SCALE ON ONE IMAGE, not a slideshow through the sixteen product
   heroes. Those are one photograph retouched per design, so cross-fading them
   would read as the cover art glitching rather than as a gallery, and the
   desktop files together weigh 3.9 MB behind the element this page is fastest
   on. The range of covers is already shown by the grid further down the page,
   where it costs nothing. Motion here costs no extra bytes at all.

   Always scaling UP, never below 1: at scale(1) the image exactly fills its
   box, so anything smaller would open a gap at the edges.

   12% over 20 seconds. CHANGED 2026-09-16 from 6% over 32s, which was
   measured running and reported by Zlatan as "no animation": three seconds in,
   the image had moved about a tenth of a percent, so in practice it never
   visibly moved. At this rate it reads as a slow push-in within a few seconds
   and still never as motion competing with the headline. `alternate` breathes
   back out instead of snapping to the start. No fill keyword: an infinite
   alternate animation has no end state to pin, and a filled one would outrank
   later declarations in the cascade. */
#home-hero .hero-bg {
    /* The origin MATCHES the crop anchor above at both widths. Scaling holds
       the origin still and pushes everything else outward, so putting it
       anywhere else would slowly walk the printed name out of frame — the one
       thing the crop comment says must never happen. */
    transform-origin: 35% center;
    animation: hero-drift 20s ease-in-out infinite alternate;
}

@media (min-width: 900px) {
    #home-hero .hero-bg {
        transform-origin: right center;
    }
}

@keyframes hero-drift {
    from {
        transform: scale(1);
    }

    to {
        transform: scale(1.12);
    }
}

@media (prefers-reduced-motion: reduce) {
    #home-hero .hero-bg {
        animation: none;
    }
}

/* The scaled image has to be contained, and `.product-intro` cannot do it:
   it is the containing block for the header, whose mobile drawer sits above
   everything at z-index 100, and a clip there would cut the drawer off.

   So the <picture> becomes the clipping box instead. It drops `display:
   contents` for this page only and takes the same `inset: 0` frame the image
   had. Out of flow, so it still generates no line box of its own, which is the
   whole reason `display: contents` was there. `clip`, not `hidden`, per the
   note on `.hero-content`. */
#home-hero picture {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: clip;
}

/* Its own crop anchor, for free-chart.html's chart photograph. Added
   2026-09-06.

   That file (astro-home-visual-background.jpg, 1500x856) is composed the
   opposite way round to the product heroes: the zodiac wheel sits between
   about 55% and 95% of the width, with empty sky to the left. .hero-bg's
   measured 35% is right for the product photograph, where the crop has to
   keep a printed NAME in frame; here it would show a phone screen of empty
   blue. 72% puts the wheel in the middle of a portrait window and still
   keeps the light burst that gives the image its centre.

   Both breakpoints are restated rather than relying on the rule above:
   this class is later in the file, so without its own desktop rule it would
   win at every width and the desktop crop would be lost. */
.hero-bg--chart {
    object-position: 72% center;
}

@media (min-width: 900px) {
    .hero-bg--chart {
        object-position: right center;
    }
}

/* The scrim is its own layer now, and that is what removes the duplication:
   the mobile and desktop gradients differ only in their second stop, yet each
   previously had to restate the image URL alongside it. */
.hero-scrim {
    position: absolute;
    inset: 0;
    z-index: 1;
    background-image: linear-gradient(to right, rgba(0, 0, 0, .6), rgba(0, 0, 0, .4));
    pointer-events: none;
}

@media (min-width: 900px) {
    .hero-scrim {
        background-image: linear-gradient(to right, rgba(0, 0, 0, .6), rgba(0, 0, 0, .2));
    }
}

@media (min-width:1052px) {
    .hero-scrim {
        background-image: linear-gradient(to right, rgba(0, 0, 0, .6), rgba(0, 0, 0, 0));
    }
}

#shop-hero .hero-scrim {
    background-image: linear-gradient(to right, rgba(0, 0, 0, .0), rgba(0, 0, 0, 0));
}

/* Positioned elements paint above non-positioned ones REGARDLESS of source
   order, so everything meant to sit on the photograph has to be positioned
   too. Without this the header and the copy render behind it even though
   they come later in the markup. */
.product-intro > .site-header,
.product-intro > .hero-content,
.product-intro > .text-center {
    position: relative;
    z-index: 2;
}

/* THE HEADER OUTRANKS THE HERO COPY. Fixed 2026-09-08 — Zlatan found the
   mobile menu opening *behind* the hero text.

   The three rules above gave the header and .hero-content the same z-index, so
   painting order fell to source order and the copy — which comes later — won.
   The open drawer's own `z-index: 100` could not save it: that number is
   resolved INSIDE the header's stacking context, and a child can never escape
   its parent's place in the stack. So the whole drawer, background included,
   was painted over.

   3 rather than a large number: it only has to beat 2, and the hero's layers
   (image 0, scrim 1, content 2) are a deliberately short ladder. */
.product-intro > .site-header {
    z-index: 3;
}

.hero-content {
    /* color: var(--c-white-soft) */
    text-shadow: var(--shadow-text);
    text-align: center;
    padding-top: var(--space-20);

    /* Contains the .hero-cta entrance. At 375px the button rests only 18px
       clear of the viewport edge, so a 2.5rem slide-in pushes it 22px past it
       and the body picks up a horizontal scrollbar for the ~0.7s the delay and
       animation last. Measured, not theoretical.

       `clip`, NOT `hidden`. `overflow-x: hidden` forces the other axis to
       compute as `auto`, which would turn the hero copy into a vertical scroll
       container; `clip` leaves overflow-y genuinely visible. It also creates no
       scroll container at all, so nothing can be scrolled into that dead space
       programmatically or by a stray focus. */
    overflow-x: clip;
}

#chapter-hero .hero-content {
    padding-bottom: var(--space-16);
}

@media (min-width: 900px) {
    .hero-content {
        text-align: left;
        padding-left: var(--space-6);
        padding-right: 38%;
    }
}

@media (min-width:1052px) {
    .hero-content {
        padding-right: 400px;
    }
}

@media (min-width:1148px) {
    .hero-content {
        padding-right: 32%;
    }
}

@media (min-width:1200px) {
    .hero-content {
        padding-right: 380px;
    }
}

.hero-heading {
    font-size: var(--fs-5xl);
    font-family: var(--font-heading-fat);
    line-height: var(--lh-snug);
    color: var(--c-white-85);
    letter-spacing: -1px;
}

.hero-text {
    font-size: var(--fs-lg);

    /* margin-bottom: var(--space-4) */
    font-weight: var(--fw-light);
    letter-spacing: 1px;
}

/* 3.5rem is a desktop size. At 375px it put the headline on four lines at 81px
   each — 325px of heading, which is most of a phone screen spent before
   anything is said. Dropped a step and tightened; the copy is unchanged and
   is Phase 2's to shorten if it still wants to. */
@media (max-width: 767px) {
    .hero-heading {
        font-size: var(--fs-4xxl);
        line-height: var(--lh-tight);
    }

    .hero-text {
        font-size: var(--fs-md);
        letter-spacing: 0;
    }
}

/* _hero_commerce — the one piece of hard sell this page is allowed.
   Added 2026-08-06.

   Rating and price, so nobody has to wonder what this costs or whether it is
   trusted, and a single CTA that scrolls into the story rather than jumping to
   the form. This is a considered, 100% personalised purchase: an impulse buy
   on it comes back as a refund and a bad review, so the first screen is
   deliberately not a checkout.

   Everything here sits on the hero photograph, so anything inheriting a
   dark-on-light colour from the buy blocks further down has to be restated. */
.hero-commerce {
    margin-top: var(--space-6);

    /* The hero's bottom breathing room used to come from the scroll cue's
       wrapper. That went with the cue, and the CTA ended up flush against the
       bottom edge of the photograph.

       Padding rather than margin, deliberately: .hero-content has no bottom
       padding of its own, so a bottom margin here collapses straight out
       through the section and buys nothing. Padding cannot collapse. It also
       keeps the fix on the one element that only exists on this page, so
       blocks/hero.html — which still has the cue — is untouched. */
    padding-bottom: var(--space-16);
}

.hero-rating {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    margin-bottom: var(--space-6);
}

.hero-rating .star-rating {
    margin-bottom: 0;
}

.hero-commerce .rating-score {
    color: var(--c-white-soft);
    font-weight: var(--fw-bold);
}

.hero-commerce .rating-count {
    color: var(--c-white-soft);
    font-size: var(--fs-sm);
    text-decoration: underline;
}

.hero-buy {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-4);
}

.hero-price {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
}

/* --c-text-soft is a 70% slate — correct on the cream price box further down,
   invisible here. */
.hero-commerce .price-text.strike-price {
    color: var(--c-white-75);
}

/* Slides in from the right a beat after load, 2026-08-06 (Zlatan). It replaced
   the scroll cue, which had its own entrance, so the hero keeps one moment of
   movement rather than losing it — and the delay means the CTA arrives after
   the headline has been read rather than competing with it.

   `backwards`, NOT `forwards` or `both` — the same trap documented on
   .scroll-cue above. A filled animation outranks normal declarations in the
   cascade, so a forwards fill would go on pinning `transform` after the
   animation ended and `.button`'s hover transform could never move it.
   `backwards` fills only during the 0.25s delay, which is exactly what holds
   the button off-screen-right until it is due; at rest the base rule applies,
   and it matches what the keyframes end on.

   The travel would overflow the viewport on a phone on its own — the button
   rests only 18px clear of the edge at 375px. `.hero-content` carries an
   `overflow-x: clip` for exactly this; see the note there. The clip is on the
   copy container rather than `.product-intro`, because the hero section cannot
   be clipped while blocks/hero.html relies on `.scroll-cue`'s -2.5rem
   overhang below it. */
.hero-cta {
    white-space: nowrap;
    animation: hero-cta-enter .45s ease-out .25s backwards;
}

/* Held back while the hero photo is still loading — see initHeroCta. Removing
   the attribute restores the animation above, which starts it afresh. */
.hero-cta[data-cta-wait] {
    animation: none;
    opacity: 0;
}

@keyframes hero-cta-enter {
    from {
        opacity: 0;
        transform: translateX(2.5rem);
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .hero-cta {
        animation: none;
    }
}

@media (min-width: 900px) {
    .hero-rating,
    .hero-buy {
        justify-content: flex-start;
    }
}

.scroll-cue {
    display: inline-block;
    width: var(--space-20);
    height: var(--space-20);
    padding: var(--space-4);
    border-radius: 50%;

    /* was 100%, merged into 50% for circles, 2026-08-03 */
    background-color: var(--c-text);
    opacity: .65;
    transition: opacity var(--transition);
    transform: scale3d(.8, .8, .8);
    margin-bottom: -2.5rem;

    /* `backwards`, NOT `both`. A finished animation with a forwards fill keeps
       holding its end state, and a filled animation outranks normal
       declarations in the cascade — so the entrance was still pinning
       `opacity` and `transform` after it ended and :hover could not move the
       cue at all (only its background-colour, which the animation does not
       touch, still responded). `backwards` fills only before the animation
       starts; at rest the base rule below applies, which is the same value
       the keyframes end on, so nothing changes visually. */
    animation: scroll-cue-enter .8s ease-out backwards;
}

.scroll-cue:hover {
    opacity: .85;
    background-color: var(--c-green);
    transform: scale3d(1, 1, 1) translateY(15px);
    transition: all var(--transition);
}

/* Slides down from the top of the page into its resting position on load. */
@keyframes scroll-cue-enter {
    from {
        opacity: 0;
        transform: translateY(-100vh) scale3d(.8, .8, .8);
    }

    to {
        opacity: .65;
        transform: translateY(0) scale3d(.8, .8, .8);
    }
}

@media (prefers-reduced-motion: reduce) {
    .scroll-cue {
        animation: none;
    }
}

.scroll-cue img {
    width: 100%;
}

/* _book_ribbon */
.intro-ribbon {
    background-color: var(--c-blue-900);
    font-family: var(--font-heading);
    color: var(--c-white-soft);
    padding: 3.5rem 1.5rem 4rem;
    border-top: 5px solid var(--c-orange);
    text-shadow: var(--shadow-text);
    text-align: center;
    background-repeat: no-repeat;
    background-size: 150%;
    background-image: url('../img/chart-drawing-blue900-transparent-1500.webp'), linear-gradient(var(--c-blue-900), var(--c-blue-800));
}

.intro-ribbon span {
    font-weight: var(--fw-regular);
}

.small-heading {
    font-weight: var(--fw-regular);
    letter-spacing: 1px;
    font-size: var(--fs-4xl);
    line-height: var(--lh-loose);
}

/* Same reasoning as .product-heading below — the family and size were coming
   from the `h1` base rule, so demoting the tag changed the design. The
   tracking and weight below were always this block's own; only the two
   inherited properties are new. */
.white-heading {
    color: var(--c-white-85);
    font-family: var(--font-heading);
    font-size: var(--fs-4xxl);
    line-height: var(--lh-tight);
    letter-spacing: 0;
    font-weight: var(--fw-semibold);
}

.cover-ribbon {
    background-color: var(--c-orange);
    padding: 0 var(--space-6);
    min-height: var(--space-12);
    width: fit-content;
    font-family: var(--font-body);
    letter-spacing: 1px;
    text-align: center;
    white-space: nowrap;
    clip-path: polygon(100% 0%, calc(100% - .75rem) 50%, 100% 100%, 0 100%, .75rem 50%, 0 0);
    color: var(--c-cream);
    background-image: var(--c-orange-gradient-400);
    min-width: 270px;
    margin: var(--space-8) auto 0;
    transform: skew(-2deg, -2deg);

    /* The pack had `box-shadow: 0 0 40px 132px #ff2e2e` here. Dropped
       2026-08-03: clip-path clips box-shadow, so that red glow never
       rendered on the live page — it was dead styling, not a colour choice. */
    padding-top: var(--space-1);
    padding-bottom: var(--space-1);
}

.cover-ribbon div {
    padding: 0;
    margin: 0;
    line-height: 1;
}

.ribbon-label {
    color: var(--c-blue-850);
    font-weight: var(--fw-regular);
    letter-spacing: 1px;
    font-size: var(--fs-xs);
    padding-top: .45rem;
}

.ribbon-title {
    font-weight: var(--fw-semibold);
    font-size: var(--fs-2xl);
    padding: .35rem 0 .45rem;
    text-shadow: var(--shadow-text);
}

/* _product_intro layout + _price_cta */
/* ==========================================================================
   _product_head — the opening block of a catalogue product page
   Added 2026-08-06, from the product-page evaluation.

   Replaces _hero + _book_ribbon + _product_intro on product.html. Those three
   rule sets above are deliberately NOT deleted: blocks/hero.html and
   blocks/book-ribbon.html still demonstrate them, and product-original.html
   is the kept before-state.

   The whole point of this block is the 375px fold. Zlatan confirmed
   2026-08-06 that the page is reached by clicking a product in a catalogue,
   so everything the click has to be paid off with — the photographs, the
   product name, the rating, the price and a way to buy — belongs above it.
   The vertical budget is therefore designed rather than incidental.
   ========================================================================== */
.product-head {
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream);
    padding-bottom: var(--space-12);
}

/* --c-header is semi-transparent by design and expects something dark behind
   it. The old hero handed it a photograph; this head is light, so the bar has
   to state a colour of its own. */
.product-head > .site-header {
    background-color: var(--c-blue-900);
}

.product-head > .site-header:hover {
    background-color: var(--c-blue-850);
}

#head-cols.row {
    align-items: flex-start;
    gap: 4%;
    padding-top: var(--space-6);
}

/* Native 1:1 — the masters are square and this shows them uncropped. Only the
   phone crops, and only because the fold there is worth more than the corners
   of the frame (see the max-width rule below). */
#head-col-gallery .image-slider .slide img {
    display: block;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

.product-title {
    font-family: var(--font-heading);
    font-size: var(--fs-4xl);
    line-height: var(--lh-tight);
    color: var(--c-blue-900);
    margin-bottom: var(--space-2);
}

.product-lede {
    font-size: var(--fs-md);
    font-weight: var(--fw-light);
    line-height: var(--lh-snug);
    margin-bottom: var(--space-4);
}

.product-rating {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

/* .star-rating stacks above the review text in a testimonial card and carries
   a bottom margin for it. Here it is one item on a single line. */
.product-rating .star-rating {
    margin-bottom: 0;
}

.rating-score {
    font-weight: var(--fw-bold);
    color: var(--c-blue-900);
}

.rating-count {
    font-size: var(--fs-sm);
    text-decoration: underline;
}

/* Project Bible 1a, the product truth: "You choose the design. The sign is
   already yours." Set apart from the lede because it is not a description. */
.cover-promise {
    margin-bottom: var(--space-6);
    padding-left: var(--space-3);
    border-left: 3px solid var(--c-orange);
    font-family: var(--font-heading);
    font-size: var(--fs-md);
    line-height: var(--lh-snug);
    color: var(--c-blue-900);
}

.buy-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-4);
}

.buy-price {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
}

/* .cart-button carries 1.5rem of its own vertical margin for the places it
   sits alone in a .button-row. Inside the buy row the flex gap already does
   that job, and the margin would only push the CTA toward the fold. */
.buy-row .cart-button {
    margin: 0;
}

.head-assurances,
.head-specs,
.author-facts {
    padding-left: 0;
    list-style: none;
}

.head-assurances {
    margin: var(--space-6) 0 0;
}

.head-specs {
    margin: var(--space-4) 0 0;
}

/* Same star bullet as .order-features used, kept as its own selector rather
   than sharing one: that rule is scoped to .order-features, which is part of
   the block this head replaced. */
.head-specs .feature-item,
.author-facts .feature-item,
.reading-detail .feature-item,
.chapter-body .feature-item {
    position: relative;
    margin-bottom: var(--space-3);
    padding-left: var(--space-8);
    background-image: url('../img/icons/astro-star-bullet.svg');
    background-repeat: no-repeat;
    background-size: 1.25rem;
    background-position: .25rem top;
    font-weight: var(--fw-light);
    line-height: var(--lh-snug);
}

/* THE CONSTRAINT THESE NUMBERS WERE MEASURED AGAINST IS GONE. The cookie
   consent banner was removed 2026-09-06; the fold at 375x812 is now the full
   812px, not the 673px a 139px fixed banner left.

   What was written here: the block was built around that 673px fold, the CTA
   ended at 642px with the banner up, and every value below was reduced to buy
   that clearance. They are kept unchanged — the block reads fine with more room
   and this is _product_head, which only product-v01.html still uses — but they
   are no longer forced, and anyone tuning them should know the pressure is off
   rather than re-deriving a limit that no longer exists. Klaro (2026-10-08)
   does not bring it back: its notice floats over the page and is gone after
   one answer, so nothing is tuned around it. */
@media (max-width: 767px) {
    #head-cols.row {
        padding-top: var(--space-2);
    }

    /* 3:2 rather than 4:3. The masters are square and the book is centred in
       every frame, so the deeper crop still costs only empty ground — and it
       is what paid for the CTA clearing the cookie banner, back when there
       was one. See the note above. */
    #head-col-gallery .image-slider .slide img {
        aspect-ratio: 3 / 2;
    }

    .product-title {
        font-size: var(--fs-3xl);
    }

    .product-lede,
    .product-rating {
        margin-bottom: var(--space-3);
    }

    .cover-promise {
        margin-bottom: var(--space-4);
    }

    .buy-row {
        gap: var(--space-3);
    }

    /* Full width on a phone: it is the one thing on the screen that has to be
       hit, and a price sitting beside it would halve the target. */
    .buy-row .cart-button {
        flex: 1 1 100%;
    }
}

/* ==========================================================================
   _showcase_photo — the photograph that used to be the hero
   Added 2026-08-06.

   Full bleed behind a headline is what made it read as decoration. Here it
   illustrates the gift argument instead, framed like every other photograph
   on the page. The art direction it had as a hero is kept: two files, and the
   crop anchor switches at the same breakpoint as the art.
   ========================================================================== */
.showcase-photo {
    margin-bottom: var(--space-8);
}

/* aspect-ratio rather than width/height attributes, because <picture> serves
   a portrait file to phones and a landscape one to desktops — one pair of
   attributes cannot describe both, and the wrong ratio is worse than none.

   4:5 on phones, and the ratio is measured rather than chosen for looks. The
   mobile file is 698x964 and carries two things that must survive the crop:
   her eyes, which start around y=150, and the name printed on the cover at
   y=790-840. A 4:3 box crops to y=220-744 and loses both ends — the reason
   the earlier pass showed a face cut off at the eyebrows. 4:5 crops to
   y=46-918 and keeps the whole of it.

   It costs 176px of page height, which is the most expensive single thing on
   this page and still worth it: this is the only photograph of a person, and
   it shows a real name printed on a real cover. That is proof, not styling. */
.showcase-photo img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 5;
    object-fit: cover;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-soft);
}

/* Same breakpoint as the art itself, or the crop and the file fight. */
@media (min-width: 900px) {
    .showcase-photo img {
        aspect-ratio: 16 / 9;
        object-position: right center;
    }
}

/* ==========================================================================
   _author_block — Marsa
   Added 2026-08-06. Project Bible 2 and 8: a publisher's author page, in
   first person, with a photo — not a testimonial.

   Sits on the cream-first gradient so it joins onto #book-reading's cream end
   and hands #book-reviews a white top. See --c-white-gradient-cream-rev.
   ========================================================================== */
.author-section {
    /* background-color: var(--c-cream) */
    /* background-image: var(--c-blue-gradient-950) */
    /* color: var(--c-white-soft) */
    padding-top: 0;

    /* padding-bottom: var(--space-14) */
    /* background-image: var(--c-white-gradient-cream-rev) */
    /* padding-top: var(--space-12) */
}

/* ---- .author-section--deep — Marsa on blue ----
   Added 2026-09-08 with the colour pass (Zlatan: "use more blue backgrounds,
   blue-light-blue-light").

   The homepage ran three light sections back to back — reading intro, Marsa,
   covers — and the middle one is the natural place to break, because it is the
   only one of the three that is about a person rather than a product. A
   portrait on deep blue also simply looks better than a portrait on cream.

   --c-blue-gradient-700 rather than the -900 the other blue bands use, and
   that is the point of the pass: the page should not be one blue repeated.
   The deep blues bracket the page (hero at the top, the starfield form at the
   bottom) and the mid-page bands sit lighter, so scrolling reads as an arc
   rather than as stripes.

   The orange star bullets on .author-facts already work on either ground, so
   only the text colour has to be restated. */
.author-section--deep {
    /* border-bottom: var(--border-blue-7) */
}

/* --shadow-soft is black, and a black shadow on deep blue is invisible — the
   portrait loses its edge and reads as a hole. A hairline in the next blue up
   gives it back. */
.author-section--deep .author-portrait img {
    /* border: 1px solid var(--c-blue-500) */
    box-shadow: var(--shadow-raised);
}

.author-row {
    align-items: flex-start;
    gap: 4%;
    text-shadow: var(--shadow-text);
}

.author-portrait img {
    display: block;
    width: 100%;
    max-width: 90%;
    height: auto;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-soft);
}

/* Above 768, the portrait matches the planet image in .reading-intro exactly.
   Zlatan, 2026-09-16.

   Two things kept them apart, and only one was visible. The 90% cap made the
   photo narrower. But the columns themselves were wider too: `.author-row
   { gap: 4% }` has never applied, because `.row { gap: 0 }` has the same
   weight and comes later in this file — so the portrait row laid out with no
   gap and each column grew to 50%. Doubling the class wins that without
   moving either rule, and the row now uses the reading block's own grid:
   48% + 4% + 48%, so the two images are equal by construction, not by a
   number typed to match. Below 768 is untouched.

   The photo is 371px wide and now shows at up to 440, so it is enlarged and
   will look soft on sharp screens. A Marsa photograph of at least 900px is
   already an open roadmap item. */
@media (min-width: 768px) {
    .row.author-row {
        column-gap: 4%;
    }

    .author-portrait img {
        max-width: 100%;
    }
}

.author-body p {
    line-height: var(--lh-relaxed);
}

.author-facts {
    margin-top: var(--space-6);
}

@media (max-width: 767px) {
    .author-portrait {
        margin-bottom: var(--space-12);
    }

    .author-portrait img {
        max-width: 320px;
        margin-left: auto;
        margin-right: auto;
    }
}

.row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-content: center;
    align-items: flex-start;
    gap: 0;
    flex-grow: 1;
}

.col-48 {
    flex: 1 1 auto;
    width: 48%;
}

.col-52 {
    width: 52%;
}

.col-4 {
    width: 50%;
}

@media (min-width: 768px) {
    .col-4 {
        width: 25%;
    }
}

@media (max-width: 767px) {
    .col-48,
    .col-52 {
        width: 100%;
    }
}

#naroci-cols.row {
    align-items: center;
    gap: 4%;
}

@media (max-width:899px) {
    #naroci-cols.row {
        align-items: flex-start;
    }
}

#naroci-cols2.row {
    justify-content: space-between;
    align-items: flex-end;
}

@media (max-width: 767px) {
    /* Stacked: constrain the column so long lines don't run the full width,
       and centre the whole block. */
    #naroci-cols.row {
        width: 100%;
        max-width: 550px;
        margin-left: auto;
        margin-right: auto;
        column-gap: 0;
    }

    /* Once stacked, the bullets sit directly under the slider and need
       breathing room that the 4% column gap was providing on wide screens. */
    #naroci-cols .order-features {
        margin-top: var(--space-12);
    }
}

/* _image_slider — auto-advancing, pauses on hover. Driven by js/blocks.js. */
.image-slider {
    position: relative;
    overflow: hidden;

    /* border-radius: var(--radius-lg) */
    background-color: var(--c-cream);
}

/* Every slide sits in the SAME grid cell and moves by its own transform, so
   the one on screen is always at translateX(0). Fixed 2026-09-16 (Zlatan saw
   photos 3 onward sitting 1px off). The old version slid one long flex track
   by -200%, -300%...: the column is a fractional width (439.68px at desktop),
   the browser snaps each slide to a whole pixel on its own, and those
   rounding errors pile up along the track — from the third photo they no
   longer cancel. A slide that is never moved off its cell has nothing to
   round. */
.image-slider .slider-track {
    display: grid;
    grid-template-columns: 100%;
}

.image-slider .slide {
    grid-area: 1 / 1;
    transition: transform .5s ease;
}

/* Before the script runs, everything but the first photo waits off to the
   right — otherwise the LAST slide, painted on top, would be the one shown. */
.image-slider .slide + .slide {
    transform: translateX(100%);
}

.image-slider .slide img {
    width: 100%;
}

/* The demo film is 16:9 and the slider is square. `cover` keeps the book,
   which is centred in frame, and loses the edges — the same crop the photos
   take on a phone. Added 2026-09-17. */
.image-slider .slide video {
    display: block;
    width: 100%;

    /* The markup's height="720" is a presentational height, and an explicit
       height beats aspect-ratio. */
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

.order-features ul.feature-list li {
    position: relative;
    margin-bottom: var(--space-3);
    padding-left: var(--space-8);
    background-image: url('../img/icons/astro-star-bullet.png');
    background-repeat: no-repeat;
    background-size: 1.05rem;
    background-position: .15rem .2rem;
}

ul.feature-list li.selected-cover {
    font-size: var(--fs-3xl);
    padding: var(--space-2) 0;
    border: 1px solid var(--c-orange);
    border-left: 0;
    border-right: 0;
    background: none;
    padding-left: 0;
    line-height: 1;
    margin-bottom: var(--space-6);
}

ul.feature-list li.selected-cover span {
    font-weight: var(--fw-bold);
    text-transform: uppercase;
    color: var(--c-orange);
}

.price-box {
    display: flex;
    justify-content: center;
    align-items: stretch;
    border-radius: var(--radius);
    background-color: var(--c-cream);
    flex-wrap: wrap;
    margin-top: var(--space-6);
    width: fit-content;
    padding: var(--space-2) var(--space-4);
    background-image: var(--c-white-gradient-cream);
    text-shadow: none;

    /* flex-shrink: 1 */
}

.price-box.landing {
    flex-wrap: nowrap;
}

#chart-to-book .price-box {
    margin-bottom: var(--space-6);
}

#covers-section .price-box,
#chart-covers .price-box {
    margin: var(--space-8) auto var(--space-12);
}

/* Must come after the base rule above, or its margin-top wins. `width:
   fit-content` means auto side margins are what keep this centred once the
   row stops being a two-column layout. */
@media (max-width: 767px) {
    .price-box,
    #chart-to-book .price-box {
        margin: var(--space-8) auto;
    }
}

.price-list.feature-list {
    margin: 0;
    padding-left: 0;
}

.feature-item.price-list {
    /* margin-top: var(--space-2) */
    margin-bottom: 0;
    padding-left: 47px;
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
    background-image: url(../img/checked-b.png);
    background-position: 0 50%;
    background-size: 36px;
    background-repeat: no-repeat;
    line-height: var(--lh-snug);
    font-weight: var(--fw-light);
    color: var(--c-blue-900);
    text-align: left;
}

.flex-column.price-amount {
    flex: 1 1 auto;
    align-items: baseline;
    margin-left: var(--space-2);
    padding: var(--space-3) 0 0 var(--space-4);
    border-left: 1px solid var(--c-tan);
    justify-content: flex-start;
    text-align: left;
    position: relative;
}

.price-text {
    color: var(--c-orange-red);
    font-size: var(--fs-3xl);
    font-weight: var(--fw-bold);

    /* text-shadow: 1px 1px 1px #999 */
}

/* A PRICE ON A LIGHT GROUND IS DARK BLUE (Zlatan, 2026-09-24). The orange
   stays wherever a price sits on dark blue — it measures 5.6:1 there and is
   the accent it is meant to be. On the cream card it measured 2.6:1, under
   the 3:1 minimum for large bold text, and a price is the one number on the
   page somebody checks. Scoped rather than set on .price-text itself, which
   lives on both grounds: the panels are cream, the product hero is not. */
.chart-confirm__product .price-text {
    color: var(--c-blue-900);
}

.price-text.strike-price {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-light);
    color: var(--c-text-soft);
    position: relative;

    /* text-decoration: line-through */
    text-shadow: none;
    display: inline-block;
    line-height: 1rem;
    margin-bottom: var(--space-2);
    margin-top: -0.5rem;
}

.price-text.strike-price::after {
    content: "";
    position: absolute;
    left: -7%;
    right: -10%;
    top: 55%;

    /* border-top: 1px solid currentColor; */
    border-top: 2px solid var(--c-blue-950-80);
    transform: rotate(-7deg);
    transform-origin: center;
    pointer-events: none;
}

/* Section backgrounds — these were inline styles on the block demo pages;
   promoted to real rules here so page compositions can just use the id/class. */
#book-intro {
    /* background-color: var(--c-white) */
    /* background-image: var(--c-white-gradient-cream) */
}

.reviews-section {
    /* background-color: var(--c-blue-900) */
    /* background-image: var(--c-blue-gradient-950) */
    text-align: center;
    padding-bottom: var(--space-12);

    /* color: var(--c-white-soft) */
}

#products-section,
.cover-gallery {
    /* background-color: var(--c-blue-900) */
    /* color: var(--c-white-soft) */
    text-shadow: var(--shadow-text);

    /* background-image: url('../img/bgr/space-galaxy.webp') */
    /* background-repeat: no-repeat */
    /* background-position: center center */
    /* background-size: cover */
    /* background-attachment: fixed */
    /* background-image: var(--c-blue-gradient-950) */
    /* background-color: var(--c-cream) */
    /* background-image: var(--c-white-gradient-cream) */
}

.cover-gallery {
    padding-bottom: var(--space-16);
}

/* The book's price, once, in the book section's own copy. Added 2026-09-16.

   Zlatan noticed the homepage did not read as though anything was for sale.
   Not on the cover cards: every cover is the same price and the heading beneath
   says so, so nine cards repeating one number would contradict it.

   The amount is its own element because in Rails it comes from the product
   record. The quiet note beside it ("with the PDF included") went on
   2026-09-17: on this site the PDF is not included with the book. The flex
   wrapper stays, so a second element can sit beside the amount again.
   Also used by free-chart.html's book section. */
.book-price {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    gap: var(--space-1) var(--space-3);
    margin: var(--space-6) 0 0;
}

.book-price__amount {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-4xl);
    line-height: 1;

    /* Dark blue: this price always sits on the cream offer card. See
       .chart-confirm__product .price-text above. */
    color: var(--c-blue-900);
    display: block;
    min-width: 75px;
}

@media (max-width:409px) {
    .book-price__amount {
        font-size: var(--fs-3xl);
    }
}

.price-box sup {
    font-size: var(--fs-md);
}

/* Left-aligned variant for a text column (free-chart.html), where a centred
   price would float away from the paragraph above it. */
.book-price--start {
    justify-content: flex-start;
    margin: var(--space-4) 0 var(--space-4);
}

/* Section header sitting on the magic-hands starfield. Not a named block in
   02b-block-inventory.md — it is _section_kicker over a full-bleed image.
   Worth naming if it turns up on a second page. */
/* `contain` reverses the 2026-08-04 switch to `cover`: the photo now shows
   whole, and --c-blue-star fills whatever the image does not reach — which
   is the reason that token exists and had to come back.
   The text sits directly on the photograph, so it carries the shadow token
   rather than relying on the image staying dark behind it. */
.astro-star-header {
    min-height: 200px;
    padding: 65vw 0 var(--space-16);
    background-color: var(--c-blue-star);
    background-image: url(../img/magic-hands-star-n.webp);
    border-top: var(--border-orange-5);
    background-repeat: no-repeat;
    background-position: center top;
    background-size: contain;
    font-family: var(--font-heading);
    color: var(--c-white-soft);
    text-shadow: var(--shadow-text);
    text-align: center;
}

.intro-container {
    /* max-width: 720px */
    /* margin: 0 auto */
}

/* _book_carousel intro row */
#products-intro {
    text-align: center;
    display: flex;
    flex-wrap: wrap;
    margin-bottom: var(--space-8);
    justify-content: center;
}

/* Was `#products-intro h1`. The catalogue heading is an h2 now that the page
   has a single h1, and a tag-scoped rule silently stops applying when the tag
   changes — this one carried the heading's 3rem size and its spacing. Matched
   to the markup 2026-08-06 rather than left to fail quietly. */
#products-intro h2 {
    margin-bottom: var(--space-8);
    font-family: var(--font-heading);
    font-weight: var(--fw-bold);
    letter-spacing: -1px;
    font-size: 3rem;
}

@media (min-width: 900px) {
    #products-intro {
        text-align: left;
        margin-bottom: var(--space-12);
        flex-wrap: nowrap;
        align-items: flex-end;
        gap: var(--space-8);
    }

    #products-intro h2 {
        margin-bottom: 0;
    }
}

/* _feature_grid */
/* .proof-grid added 2026-09-06 with home.html: this is #book-proof-box turned
   into a class, and nothing about the treatment was ever page-specific — it
   was written as an ID only because product.html was the only page that
   existed. The ID stays so product.html is untouched by the homepage build;
   it adopts the class in the batch that translates it. */
#book-proof-box,
.proof-grid {
    /* color: var(--c-white-soft) */
    /* padding-top: var(--space-12) */
    padding-bottom: var(--space-12);
    text-shadow: var(--shadow-text);
}

/* The same grid on the light ground, added 2026-09-06 for free-chart.html.
   That page is short and its dark moment is the form; a second blue band
   above it would leave the page with no contrast at all. The orange feature
   icons were always readable on both grounds — only the text colour and the
   background change. */
.proof-grid--light {
    color: var(--c-text);
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream);
    padding-bottom: var(--space-12);
}

/* Colour comes from .astro-stars-bg, which this element always carries. */
.book-highlights {
    text-align: center;
    padding-bottom: var(--space-12);
}

.book-highlights .row {
    padding: var(--space-8) 0;
    row-gap: var(--space-8);
}

.book-highlights .col-4,
#book-proof-box .col-4,
.proof-grid .col-4 {
    padding: var(--space-4);
}

/* Two mobile length-cuts lived here (proof-grid icons shrunk, planet drawing
   capped). Reverted 2026-08-06 when product.html was restored: they were part
   of the length pass, which is being reconsidered, and both are shared rules
   that would have altered the restored page without anyone asking for it.
   product-v01.html measures ~0.3 screens taller than reported because of it. */
.feature-icon {
    display: inline-block;
    width: var(--space-20);
    height: var(--space-20);
    margin: 0 auto;
    border-radius: 50%;
    background-color: var(--c-orange);
    background-position: 50% 50%;
    background-repeat: no-repeat;
    background-size: 3.5rem;
}

.feature-icon.quality {
    background-image: url('../img/icons/i-quality.png');
}

.feature-icon.magic {
    background-image: url('../img/icons/i-magic.png');
}

.feature-icon.delivery {
    background-image: url('../img/icons/i-delivery.png');
}

.feature-icon.shield {
    background-image: url('../img/icons/i-shield.png');
}

.feature-icon.unique {
    background-image: url(../img/bullet-unique.webp);
}

.feature-icon.knjiga {
    background-image: url(../img/bullet-book.webp);
}

.feature-icon.stars {
    background-image: url('../img/icons/stars-white.svg');
}

.feature-icon.chart {
    background-image: url('../img/icons/i-chart.svg');
}

.feature-icon.reading {
    background-image: url(../img/bullet-razlaga.webp);
}

.feature-title {
    margin: var(--space-4) 0 var(--space-3);
    font-weight: var(--fw-bold);
    font-size: var(--fs-lg);
}

#our-exact .feature-title {
    color: var(--c-orange);
    margin-bottom: var(--space-2);
}

.feature-caption {
    margin-top: var(--space-4);
}

/* _two_col_row */
.reading-intro {
    /* color: var(--c-white-soft) */
    text-shadow: var(--shadow-text);
    padding-bottom: var(--space-16);
    padding-top: var(--space-16);

    /* background-image: url('../img/bgr/space-planets.webp') */
    /* background-repeat: no-repeat */
    /* background-position: right center */
    /* background-size: cover */
    /* background-attachment: fixed */
    /* background-color: var(--c-cream) */
    /* background-image: var(--c-white-gradient-cream) */
    /* The section closes on its own padding rather than on the CTA's bottom
       margin. It used to end with the Marsa pull-quote, so that margin was
       internal and harmless; with the quote moved to #marsa the CTA became
       the last child and its margin collapsed straight out of the section,
       showing 80px of white body between this and the block below. */
}

#chart-to-book.reading-intro {
    padding-top: var(--space-16);
}

.reading-intro .row {
    align-items: stretch;
    column-gap: 4%;
    row-gap: 3rem;
}

@media (max-width: 767px) {
    .reading-intro {
        text-align: center;
    }

    .reading-image {
        /* margin-bottom: 2.5rem */
        /* max-width: 500px */
        margin-left: auto;
        margin-right: auto;

        /* margin-bottom: var(--space-12) */
    }
}

/* Heading LOOK belongs to the class; heading LEVEL belongs to the document.
   Filled in 2026-08-06, and it was an empty rule until then.

   The two are very differently styled here — `h1` is bold Lora at --fs-5xl
   with -1px tracking, `h2` is light Source Sans at --fs-4xl. So the base tag
   was carrying the design, and the heading level could not be corrected for
   SEO without redesigning the page. When this page went from 8 `h1`s to 1,
   six section headings silently changed typeface, size and weight — serif to
   sans, bold to light — while the change was being described as markup-only.

   This class now states the look outright, so the tag is free to describe the
   document and nothing else. Same values the `h1` base rule gives, so the
   headings render exactly as they did before the demotion. */
.product-heading {
    font-family: var(--font-heading);
    font-size: var(--fs-4xxl);
    font-weight: var(--fw-bold);
    letter-spacing: 0px;
    line-height: var(--lh-tight);
    color: var(--c-white-85);
}

#shop-catalogue .intro-container .product-heading {
    max-width: 600px;
    margin-left: auto;
    margin-right: auto;
}

/* Same step-down .hero-heading got: 3rem on a phone put a long heading on five
   lines, most of the screen before anything is said. One step smaller, and
   still one step below the hero's phone size. Zlatan, 2026-10-08. */
@media (max-width: 767px) {
    .product-heading {
        font-size: var(--fs-4xl);
    }
}

.reading-intro .col-48 img {
    border-radius: 17px;
}

#chart-to-book.reading-intro .col-48 img {
    border-radius: 0px;
}

@media (max-width: 767px) {
    .reading-intro .col-48 img {
        border-radius: var(--radius-lg);
    }
}

/* _marsa_quote */
.quote-section {
    background-image: url('../img/marsa-t.png');
    background-repeat: no-repeat;
    background-position: right bottom;
    background-size: 33%;
    padding-bottom: var(--space-12);
}

.quote-card {
    position: relative;
    min-height: 50px;
    padding: var(--space-4);
    border-radius: .75em;
    background-color: var(--c-white-soft);
    color: var(--c-blue-700);
    box-shadow: var(--shadow-tight);
    margin-right: 35%;
    text-align: left;
}

.quote-text {
    font-size: var(--fs-md);
    font-family: var(--font-heading);
    font-weight: var(--fw-thin);
    font-style: italic;
}

.quote-signature {
    font-size: var(--fs-xs);
    color: var(--c-text);
    margin-top: .75rem;
    border-top: 1px dotted var(--c-blue-800);
    padding-top: var(--space-2);
}

.quote-signature b {
    color: var(--c-orange-200);
}

.quote-card:after {
    content: '';
    position: absolute;
    right: 0;
    top: 80%;
    width: 0;
    height: 0;
    border: 1em solid transparent;
    border-left-color: var(--c-white-soft);
    border-right: 0;
    border-bottom: 0;
    margin-top: -.5em;
    margin-right: -1em;
}

@media (min-width: 768px) {
    .quote-section {
        background-size: contain;
    }

    .quote-card {
        margin-left: 5%;
    }

    .quote-card:after {
        top: 70%;
    }
}

@media (min-width: 868px) {
    .quote-card {
        margin-left: 10%;
        margin-right: 30%;
    }

    .quote-card:after {
        top: 60%;
    }
}

/* Shared starfield section background — used by the 3D book section, the
   highlights grid and the chart form section. */
/* The starfield owns its own text colour. Anything laid on it is on a dark
   surface by definition, so the colour belongs here once rather than being
   restated by every section that uses it.

   The corollary: a LIGHT panel sitting inside a starfield section now has to
   declare its own text colour, because it no longer gets the dark default by
   inheritance — see `.confirm-card`. */
.astro-stars-bg,
.astro-stars-math,
.astro-stars-about {
    background-color: var(--c-blue-900);
    background-image: url('../img/bgr/space-galaxy.webp');
    background-repeat: no-repeat;
    background-size: cover;

    /* background-attachment: fixed */
    color: var(--c-white-soft);
    background-position: center center;
}

/* The My astro pages' starfield (Zlatan, 2026-10-15): the same ground with
   another picture, so it shares the rule above and swaps only the image. */
.astro-stars-math {
    background-image: url('../img/bgr/space-math.webp');
}

.astro-stars-about {
    background-image: url('../img/bgr/space-planets-show.webp');
}

@media (min-width:768px) {
    .astro-stars-about {
        background-image: url('../img/bgr/space-planets-show-w.webp');
    }
}

/* _3d_book_viewer — section framing only. The book itself lives in
   css/book3d.css and js/book3d.js (built 2026-08-04).
   `.book-display` and `.viewer-controls` were removed with the static shell
   they styled; the live block uses .book3d__stage and .book3d__controls. */
/* Colour comes from .astro-stars-bg, which this element always carries. */
.showcase-intro {
    text-align: center;

    /* border-top: var(--border-orange-5) */
    position: relative;
    background-color: var(--c-cream);
}

.book-viewer {
    z-index: 10;
    position: relative;
    text-align: center;

    /* The book's ground shadow is deliberately wider than the book — up to
       1.28x a cover — so that it spreads past its own footprint. When the
       book is sized by the 92vw term rather than by 80vh (any portrait or
       narrow window), that spread runs past the right edge of the viewport
       and gives the whole page a horizontal scrollbar.

       Clipped here, at the full-bleed wrapper, because this is the only
       ancestor as wide as the viewport: anything narrower would cut visible
       shadow instead of just the part nobody can see. `clip` rather than
       `hidden` so no scroll container is created — `hidden` would compute
       overflow-y to `auto` and could trap the shadow in an inner scroller.
       `hidden` stays first as the fallback for Safari below 16.

       Safe for the 3D scene: a non-visible overflow forces `transform-style:
       flat` only on the element carrying it, and this element is not part of
       the preserve-3d chain, which starts below it at .book3d__book. */
    /* overflow-x: hidden; */
    overflow-x: clip;
    padding-top: var(--space-8);
    padding-bottom: var(--space-16);
}

/* _chart_picker */
ul.chart-list {
    list-style: none;
    padding: 0;
    margin: 0 auto;
    background: var(--c-orange);
    text-align: center;
    color: var(--c-text);
    max-width: var(--max-form-width);
}

ul.chart-list li {
    display: block;
    position: relative;
    width: 100%;
    text-transform: uppercase;
}

ul.chart-list li ul {
    position: absolute;
    top: 100%;
    left: 0;
    display: block;
    width: 100%;
    z-index: 2;
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    visibility: hidden;
    transition: max-height var(--transition), opacity var(--transition), visibility 0s linear var(--transition);
}

/* A chart's name may run to 36 characters, and on this list the whole name
   has to be read — the wrong one ends up printed on a cover. So an entry
   wraps rather than being cut off (2026-10-01: "Štefan Aleksander
   Bogosavljević" was clipped under the arrow on a phone). Was nowrap +
   hidden. The sides are Zlatan's own (he set them back the same day). */
ul.chart-list li a {
    display: block;
    padding: var(--space-4);
    padding-right: var(--space-8);
    text-decoration: none;
    color: var(--c-white-soft);
    border-bottom: 1px solid var(--c-black-25);
}

ul.chart-list li:first-child > a {
    border-top: 1px solid var(--c-black-25);
}

/* Opens on hover (mouse), on .open (click/tap, toggled by blocks.js) and on
   :focus-within (keyboard, works even before blocks.js has run). */
ul.chart-list li:hover > ul,
ul.chart-list li.open > ul,
ul.chart-list li:focus-within > ul {
    max-height: 20rem;

    /* Scrolls inside its 20rem rather than hiding the end of the list: a
       reader may have ten charts, and wrapped names make each one taller
       (2026-10-01 — the fourth sample chart was half cut off on a phone). */
    overflow-y: auto;
    opacity: 1;
    visibility: visible;
    transition: max-height var(--transition), opacity var(--transition), visibility 0s linear;
}

ul.chart-list li.charts-label:hover,
ul.chart-list li ul {
    /* box-shadow: var(--shadow-tight) */
}

/* The submenu is opaque on its own, not only while hovered — it can now be
   opened by click and by keyboard, where no :hover state exists. */
ul.chart-list li ul {
    background: var(--c-orange-red);
}

ul.chart-list li ul a {
    background-color: var(--c-orange);
    background-image: url('../img/icons/arrow-right.svg');
    background-repeat: no-repeat;
    background-size: .6rem;
    background-position: left 1.25rem center;
    padding: var(--space-4) var(--space-4) var(--space-4) var(--space-8);
}

ul.chart-list li ul a:hover,
ul.chart-list li ul a:focus-visible {
    background-color: var(--c-green);
    color: var(--c-white-soft);

    /* background-image: url('../img/icons/arrow-right.svg') */
}

ul.chart-list li ul a.added {
    background: var(--c-green);
    color: var(--c-white-soft);
}

/* The chevron is the saved-chart bar's arrow turned to point down (Zlatan,
   2026-09-30: "unify right and down arrow") — same thin shape, same size,
   same place, and the same side padding, so the label centres the same way
   in both. Was the heavier icon-down-white(-75).svg — and since the same day
   every down arrow on the site is this one (Zlatan: "replace them all with
   thin"): the catalogue sort, .button--down, the phone summary fold and the
   chapter menu toggle. */
.charts-label span {
    padding: var(--space-4) 3.5rem;
    display: block;
    background: var(--c-orange);
    color: var(--c-white-soft);

    /* The dropdown's title is a label, like "Use this birth chart": small,
       so the names in the list are what stands out (Zlatan, 2026-10-01:
       "smaller"). Was --fs-lg, bold. */
    font-size: var(--fs-sm);
    background-image: url('../img/icons/chevron-down-white.svg');
    background-repeat: no-repeat;
    background-size: 1.1rem;
    background-position: right 1.5rem center;
    cursor: pointer;
}

.charts-label span:focus-visible {
    outline: 2px solid var(--c-green);
    outline-offset: -2px;
}

/* While the list is open the label steps back, so the eye goes to the charts
   (Zlatan, 2026-10-01). The same three conditions that open the list — hover,
   .open from a click or tap, and keyboard focus — so the two never disagree.
   On the span only: the list is inside the same <li> and must stay solid. */
.charts-label > span {
    transition: opacity var(--transition);
    color: var(--c-text);
}

ul.chart-list li.charts-label:hover > span,
ul.chart-list li.charts-label.open > span,
ul.chart-list li.charts-label:focus-within > span {
    opacity: .5;
}

/* ---- _saved_charts — the reader's own birth charts, 0 / 1 / many ----
   Agreed 2026-09-30 (Zlatan): every chart form knows how many charts the
   reader already has. data-charts-state="zero|one|many" sits on <body>, like
   data-owner-state, so it reaches the order panel outside <article>; Rails
   sets it from the real count (signed out = zero). Every version's copy is in
   the markup; a part may belong to two states (data-charts-part="one many").

   Order forms: zero = the form, with a sign-in line; one = "Use this birth
   chart" (no dropdown with a single entry — it reads as broken); many = the
   _chart_picker dropdown. Free-chart forms: only a quiet shortcut above the
   form, because someone there came to make a new chart (the reason recorded
   on free-chart.html, 2026-09-26). */
[data-charts-state="zero"] [data-charts-part]:not([data-charts-part~="zero"]),
[data-charts-state="one"] [data-charts-part]:not([data-charts-part~="one"]),
[data-charts-state="many"] [data-charts-part]:not([data-charts-part~="many"]) {
    display: none;
}

/* The one saved chart, in the dropdown's slot and colours: an orange bar,
   the white arrow where the dropdown has its chevron. */
.saved-chart {
    display: block;
    max-width: var(--max-form-width);
    margin: 0 auto;
    padding: var(--space-4) 3.5rem;
    background-color: var(--c-orange);
    background-image: url('../img/icons/arrow-right.svg');
    background-repeat: no-repeat;
    background-size: .6rem;
    background-position: right 1.5rem center;
    color: var(--c-white-soft);
    text-align: center;
    text-decoration: none;
    transition: background-color var(--transition), border-color var(--transition);
}

.saved-chart:hover,
.saved-chart:focus-visible {
    background-color: var(--c-green);
    color: var(--c-white-soft);
}

.saved-chart__label {
    display: block;
    font-size: var(--fs-sm);
    color: var(--c-text);

    /* font-weight: var(--fw-bold) */
    text-transform: uppercase;
}

/* The chart's name and birth date: data, not copy. Its own element, so the
   sentence around it stays a text row (house style, 2026-09-21). */
.saved-chart__name {
    display: block;
    font-size: var(--fs-base);
}

/* On a free-chart form it is a shortcut, not the main action: outlined, so it
   does not compete with the form below it. UNUSED since 2026-09-30 — the one
   saved chart became a real button (.button.outline.blue, Zlatan: "make link a
   button"); kept, as rules are, for the block demo history. */
.saved-chart--quiet {
    background-color: transparent;
    background-image: url('../img/icons/arrow-right-blue.svg');
    border: 1px solid var(--c-border);
    color: var(--c-text);
}

.saved-chart--quiet:hover,
.saved-chart--quiet:focus-visible {
    background-color: transparent;
    border-color: var(--c-orange);
    color: var(--c-text);
}

/* The free-chart form's dropdown, "Open one of your birth charts" (Zlatan,
   2026-09-30: "dropdown list as it is on order, but keep current design").
   The order panel's _chart_picker — same list, same keyboard — with the
   trigger in the quiet outlined look of .saved-chart--quiet and a blue
   chevron. The list itself stays the order panel's. */
ul.chart-list--quiet {
    background: transparent;
}

.chart-list--quiet .charts-label span {
    background-color: transparent;
    background-image: url('../img/icons/chevron-down-blue.svg');
    border: 1px solid var(--c-border);
    color: var(--c-text);
}

.chart-list--quiet .charts-label span:hover,
.chart-list--quiet .charts-label span[aria-expanded="true"] {
    border-color: var(--c-orange);
}

ul.chart-list--quiet li.charts-label:hover {
    box-shadow: none;
}

/* The chart's name and date as the second line inside a button. */
.button .saved-chart__name {
    margin-top: .1rem;
    text-transform: none;
}

/* Between a saved chart and the form: "or calculate a new one". */
.charts-or {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    max-width: var(--max-form-width);
    margin: var(--space-4) auto;
    font-size: var(--fs-sm);
    color: var(--c-text-soft);
    text-transform: uppercase;
    letter-spacing: 1px;
}

.charts-or::before,
.charts-or::after {
    content: '';
    flex: 1;
    border-top: 1px solid var(--c-border);
}

/* _coupon_box */
.coupon-card {
    /* margin: 0 auto */
    /* padding: var(--space-16) var(--space-6) */
    /* background-color: var(--c-blue-950) */
    /* border-radius: 1rem */
    /* background-color: var(--overlay-scrim) */
    /* text-align: center */
    /* color: var(--c-white-soft) */
    /* max-width: var(--max-form-width) */
    /* border-top: 7px solid var(--c-blue-700) */
}

/* ORANGE, AND THAT MAKES IT A DARK-GROUND CLASS. It exists for the coupon
   card, which sits on the starfield — --c-orange on --c-blue-900 is a
   comfortable read.

   On cream it measures 1.56:1, which is not a heading, it is a watermark.
   Caught 2026-09-08 by a contrast audit after the colour pass: home.html had
   borrowed this class for a 28px heading on the light covers section. Use
   .section-subheading below on anything light. */
.small-subheading {
    font-size: var(--fs-2xl);
    color: var(--c-orange);
    letter-spacing: 1px;
}

/* The same heading with no colour of its own, so it inherits whatever ground
   it lands on. Added 2026-09-08. Two classes rather than one class plus an
   override, because the house rule is that a heading class states its look
   outright — a heading whose colour depends on where you put it is exactly
   the trap that made a tag change repaint six headings on the product page. */
.section-subheading {
    font-size: var(--fs-2xl);
    letter-spacing: 1px;
}

/* ==========================================================================
   Cards — _book_card, _book_carousel, _testimonial_card, _star_rating
   ========================================================================== */
a.product-card {
    width: 100%;
    min-width: 100%;

    /* border-radius: .25rem */
    /* border: 1px solid var(--c-black-25) */
    color: var(--c-blue-950);
    background: var(--c-cream);
    display: block;
    position: relative;
    transition: var(--transition);
}

@media (min-width: 540px) {
    a.product-card {
        width: 49%;
        min-width: 49%;
    }
}

@media (min-width: 900px) {
    a.product-card {
        width: 32%;
        min-width: 32%;
    }
}

a.product-card:hover {
    /* border-color: var(--c-green) */
    background-color: var(--c-green);

    /* color: #000; */
    box-shadow: var(--shadow-soft);
}

a.product-card:hover .card-content,
a.product-card:hover .card-plus {
    opacity: 1;

    /* background-color: var(--c-cream-soft); */
}

/* a.product-card:hover .card-content h3 {
    font-weight: 400;
} */
.card-image {
    overflow: hidden;

    /* border-top-left-radius: .25rem */
    /* border-top-right-radius: .25rem */
}

.card-image img {
    /* border-top-left-radius: .25rem */
    /* border-top-right-radius: .25rem */
    transition: var(--transition);
}

a.product-card:hover .card-image img {
    transform: scale3d(1.05, 1.05, 1.05);
}

/* Photo rotator that slides (data-photo-slide, 2026-10-01). The next photo
   waits off to the right and moves in while the current one moves out to
   the left. The first selector group zeroes the img's own hover transition
   so only the slide is animated. Reduced motion never starts a rotator, so
   it needs no override, but the transition is dropped for it anyway. */
.card-image--slide {
    position: relative;
}

.card-image--slide img {
    display: block;
    width: 100%;
    height: auto;
    transition: transform .8s ease;
}

.card-image--slide .card-slide {
    position: absolute;
    inset: 0;
    height: 100%;
    object-fit: cover;
    transform: translateX(100%);
}

.card-image--slide .card-slide.is-entering {
    transform: translateX(0);
}

.card-image--slide img.is-leaving {
    transform: translateX(-100%);
}

@media (prefers-reduced-motion: reduce) {
    .card-image--slide img {
        transition: none;
    }
}

/* UNUSED since 2026-09-18 — Zlatan removed "View more" from every card: it
   covered the book in the middle of the photo, and the card already says it
   is a link. Kept, per the house style, in case it comes back.

   "View more" in the centre of a cover card, on hover. Zlatan, 2026-09-16.

   A span styled as a button, not a <button>: the whole card is already the
   link, and a button inside a link is invalid HTML. Clicking it follows the
   card. aria-hidden, because a screen reader already hears the card's link and
   its cover name; "View more" on top of that is noise.

   Keyboard focus shows it too, so it is not a mouse-only cue. Hover is gated
   on (hover: hover): on a phone a tap follows the link at once, and without the
   gate the label would flash and stick under the finger. The card centre is
   the photo centre, because the name and badges are absolutely positioned and
   add no height. */
.card-more {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 2;
    padding: var(--space-2) var(--space-4);
    border: 2px solid var(--c-white-85);
    border-radius: var(--radius);
    background-color: var(--c-blue-950-80);
    color: var(--c-white);
    font-size: var(--fs-sm);
    white-space: nowrap;
    text-shadow: none;
    opacity: 0;
    transform: translate(-50%, -35%);
    transition: opacity var(--transition), transform var(--transition);
    pointer-events: none;
}

@media (hover: hover) {
    a.product-card:hover .card-more {
        opacity: 1;
        transform: translate(-50%, -50%);
    }
}

a.product-card:focus-visible .card-more {
    opacity: 1;
    transform: translate(-50%, -50%);
}

@media (prefers-reduced-motion: reduce) {
    .card-more {
        transform: translate(-50%, -50%);
        transition: opacity var(--transition);
    }
}

.card-content {
    padding: var(--space-4) 1.25rem;
    position: absolute;
    right: 0;
    bottom: 0;
    background-color: var(--c-cream-light);
    border-top-left-radius: var(--radius-sm);
    text-align: right;
    text-shadow: none;
    opacity: .7;
    padding: var(--space-3) var(--space-6);

    /* background-image: var(--c-gradient-cream) */
}

.card-label {
    /* padding: var(--space-4) 1.25rem; */
    position: absolute;
    background-color: var(--c-text-soft);
    border-radius: var(--radius-sm);
    font-size: var(--fs-xl);
    color: var(--c-white-85);

    /* text-align: right */
    text-shadow: none;
    opacity: .85;
    padding: var(--space-1) var(--space-4);
    top: .75rem;
    left: .75rem;
    font-weight: 800;

    /* background-image: var(--c-gradient-cream) */
}

.price-amount .card-label {
    position: static;

    /* top: .75rem */
    /* left: .75rem */
}

.card-label.price {
    font-size: var(--fs-lg);
}

.card-label.price span {
    color: rgba(255, 255, 255, 0.5);
    font-weight: 400;
    text-decoration: line-through;

    /* background-image: var(--c-gradient-cream) */
}

.card-label.orange {
    background-color: var(--c-orange);
}

.card-plus {
    position: absolute;
    text-shadow: none;
    top: .85rem;
    right: .85rem;
    font-weight: 800;
    min-width: 1.15rem;
    min-height: 1.15rem;
    opacity: .7;

    /* background-image: var(--c-gradient-cream) */
}

@media (min-width:768px) {
    .card-plus {
        min-width: 1rem;
        min-height: 1rem;

        /* background-image: var(--c-gradient-cream) */
    }
}

.card-content h3 {
    font-weight: var(--fw-bold);
    text-transform: uppercase;

    /* color: var(--c-text) */
}

.card-content b {
    color: var(--c-orange-red);

    /* font-size: var(--fs-xs) */
}

.card-content .button {
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-sm);
    letter-spacing: 0;
    background-color: var(--c-orange);
}

a.product-card:hover .card-content .button {
    background-color: var(--c-green);
}

.carousel-wrapper {
    position: relative;
}

@media (min-width:1052px) {
    .carousel-wrapper {
        margin-bottom: var(--space-6);
    }
}

#products-carousel {
    display: flex;
    align-items: stretch;
    width: 100%;
    gap: 2%;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
}

#products-carousel::-webkit-scrollbar {
    display: none;
}

#products-carousel:focus-visible {
    outline: 2px solid var(--c-green);
    outline-offset: var(--space-2);
}

#products-carousel > .product-card {
    scroll-snap-align: start;
    flex: 0 0 auto;
}

.carousel-nav {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 2.5rem;
    gap: var(--space-4);
}

.carousel-nav a {
    display: inline-block;
    width: 2.5rem;
    height: 2.5rem;
    background-color: var(--c-blue-800);
    border-radius: 50%;
    padding: .55rem;
    opacity: .5;
    transition: opacity var(--transition), background-color var(--transition);
}

.carousel-nav a:first-child img {
    transform: rotate(180deg);
}

.carousel-nav a:hover {
    opacity: 1;
    background-color: var(--c-green);
}

.carousel-nav a:focus-visible {
    outline: 2px solid var(--c-green);
    outline-offset: var(--space-1);
}

.carousel-nav a[aria-disabled="true"] {
    opacity: .15;
    pointer-events: none;
}

/* Arrows sit fully outside .w-container, so the cards themselves fill the
   container edge to edge. That needs 3.5rem of clear space beyond the
   container on each side, which only exists once the viewport is wider than
   the container plus both gutters — 940px + 2 x 56px = 1052px. Below that
   there is nowhere to put them without clipping, so they stay in flow
   beneath the carousel. Special-case breakpoint, one component only,
   per the convention in tokens.css. */
@media (min-width: 1052px) {
    .carousel-nav {
        margin-top: 0;
    }

    .carousel-nav a {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
    }

    .carousel-nav a:first-child {
        left: -3.5rem;
    }

    .carousel-nav a:last-child {
        right: -3.5rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    #products-carousel {
        scroll-behavior: auto;
    }
}

.w-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: stretch;
}

.review-card {
    padding: var(--space-4) var(--space-3);
    border: 1px solid var(--c-black-25);
    border-radius: var(--radius-lg);
    background: var(--c-white-85);
    text-align: left;
    transition: box-shadow var(--transition), background-color var(--transition);
    display: flex;
    width: 100%;
}

.reading-intro .review-card {
    text-shadow: none;
}

@media (min-width: 768px) {
    .review-card {
        flex: 1;
        min-width: 40%;
    }
}

.review-card:hover {
    background: var(--c-white);
    box-shadow: var(--shadow-tight);
}

/* Progressive reveal — the reviews arrive four at a time.
   `display: none` rather than a visually-hidden trick, so the hidden cards
   take no space in the flex row and the section is honestly short until the
   reader asks for more. */
.review-card--pending {
    display: none;
}

/* `backwards`, not `both`: the end state is the card's natural one, and a
   forwards fill would pin `transform` for good — the same thing that had
   silently disabled the .scroll-cue hover. Backwards still suppresses the
   flash before a staggered card's turn comes. */
.review-card--revealed {
    animation: review-reveal 420ms ease-out backwards;
    animation-delay: calc(var(--reveal-i, 0) * 70ms);
}

@keyframes review-reveal {
    from {
        opacity: 0;
        transform: translateY(18px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .review-card--revealed {
        animation: none;
    }
}

.review-avatar {
    margin-right: var(--space-3);
    width: 3.5rem;
    height: 3.5rem;
    min-width: 3.5rem;
    min-height: 3.5rem;
}

/* The initials disc (Zlatan, 2026-10-01: no reviewer photos). The reviewers'
   Etsy avatars were other people's faces; a stock face on a real review reads
   as fake, so the disc is the first letter of the name, nothing to download.
   data-disc picks one of five blues/oranges; in Rails it is a hash of the name,
   so a person keeps the same colour on every page. The 5-colour set is the
   site's own, never a rainbow. */
.review-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--c-blue-800);
    color: var(--c-white-soft);
    font-family: var(--font-heading-fat);
    font-size: var(--fs-lg);
    line-height: 1;
    text-transform: uppercase;
    user-select: none;
}

.review-avatar[data-disc="1"] {
    background: var(--c-blue-800);
}

.review-avatar[data-disc="2"] {
    background: var(--c-blue-700);
}

.review-avatar[data-disc="3"] {
    background: var(--c-blue-500);
}

.review-avatar[data-disc="4"] {
    background: var(--c-orange);
    color: var(--c-blue-800);
}

.review-avatar[data-disc="5"] {
    background: var(--c-orange);
    color: var(--c-blue-900);
}

.review-text {
    color: var(--c-grey-text);
}

.review-text.review-byline {
    font-size: var(--fs-xs);
    font-style: normal;
    color: var(--c-text);
    margin-bottom: .25rem;
}

.review-text.review-byline span {
    font-weight: var(--fw-bold);
}

.star-rating {
    margin-bottom: var(--space-3);
    display: flex;
}

.star-rating img {
    max-height: 1rem;
    margin-right: .2rem;
}

.star-rating img.empty-star {
    opacity: .25;
}

/* ==========================================================================
   _proof_strip — the accuracy line under the reading introduction
   Added 2026-09-06 with home.html.

   Bible 5: the largest non-brand search cluster we get is people looking up
   their rising sign, so a stranger's first doubt is whether the calculation
   is real rather than whether the writing is good. This states it once and
   then gets out of the way — Bible 7 keeps the automated side of the business
   low-key, and a whole section about ephemerides would break that.
   ========================================================================== */
.proof-strip {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    max-width: 720px;
    margin: var(--space-6) auto 0;
    padding-top: var(--space-6);
    border-top: 1px dotted var(--c-orange);
    font-size: var(--fs-sm);

    /* color: var(--c-text-soft) */
}

/* The logo strip is 825x200 and full of small type, so it stops shrinking
   before it becomes unreadable and the caption wraps under it instead.
   UNUSED since 2026-09-26: the logos gave way to .proof-mark below. Kept, per
   house style, until nothing wants it back. */
.proof-strip img {
    flex: 0 0 auto;
    width: 200px;
}

/* .proof-mark — the source named in words, in the site's own type (Zlatan,
   2026-09-26: "Replace nasa logo"). Two lines, the way the logo strip read:
   where the data comes from, small and spaced like a .section-label, then
   the engine that turns it into positions, in the heading face. A dotted
   frame in the label's orange, square corners (house style 2026-09-17).
   flex: none, so the caption beside it takes the width, as it did with the
   image. */
.proof-mark {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 0;
    padding: var(--space-3) var(--space-6);
    border: 1px dotted var(--c-orange-50);
    text-align: center;
}

.proof-mark__source {
    font-family: var(--font-body);
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--c-orange);
}

.proof-mark__engine {
    font-family: var(--font-heading);
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    line-height: var(--lh-tight);
    color: var(--c-white-85);
}

@media (max-width: 767px) {
    .proof-strip {
        flex-direction: column;
        text-align: center;
        gap: var(--space-4);
    }
}

@media (max-width:899px) {
    .proof-strip {
        margin-top: 0px;
    }
}

/* ==========================================================================
   _cover_gallery — the catalogue preview on the homepage
   Added 2026-09-06.

   A GRID, not a carousel, and that is the whole point of the block. The
   reaction the page list is aiming for is "there are a lot of beautiful
   books"; a carousel shows two of them and hides the rest behind a swipe,
   which says the opposite. _book_carousel keeps its job at the foot of the
   product page, where a sideways glance is exactly right.

   Phones: one column. From 768px, a MIXED grid (Zlatan, 2026-09-17): the
   first cover large on the left at half the width, four small ones beside it
   in two rows, then three to a row. One large cover gives the eye somewhere
   to land before the set; a flat grid of equals has no first thing to look at.

   Twelve columns so both rhythms fit one grid: 6 + 3 + 3 on top, 4 + 4 + 4
   below. The cards are square with everything laid over the photo, so a card
   spanning two columns and two rows is exactly as tall as two small ones and
   the gap — nothing to align by hand. The home set is eight covers, which
   fills this exactly (5 + 3). A set of another size still lays out, with a
   short last row.
   ========================================================================== */
.cover-grid {
    display: grid;
    grid-template-columns: repeat(1, 1fr);
    gap: var(--space-4);
}

@media (min-width: 768px) {
    .cover-grid {
        grid-template-columns: repeat(12, 1fr);
    }

    .cover-grid > .cover-card {
        grid-column: span 4;
    }

    .cover-grid > .cover-card:first-child {
        grid-column: span 6;
        grid-row: span 2;
    }

    .cover-grid > .cover-card:nth-child(n+2):nth-child(-n+5) {
        grid-column: span 3;
    }

    /* The four small covers are a quarter of the width — 174px at 768. The
       name plate and badges are sized for a full card and would cover half
       the photo, so they step down here. */
    .cover-grid > .cover-card:nth-child(n+2):nth-child(-n+5) .card-content {
        padding: var(--space-2) var(--space-3);
    }

    .cover-grid > .cover-card:nth-child(n+2):nth-child(-n+5) .card-content h3 {
        font-size: var(--fs-xs);
    }

    .cover-grid > .cover-card:nth-child(n+2):nth-child(-n+5) .card-label {
        font-size: var(--fs-base);
        padding: 0 var(--space-2);
    }

    .cover-grid > .cover-card:nth-child(n+2):nth-child(-n+5) .card-label ~ .card-label {
        top: 2.75rem;
    }
}

/* a.product-card carries its own carousel widths (100/49/32%) and a
   min-width to match, which would fight the grid track. Inside a grid the
   track is the width, so both are handed back. */
.cover-grid > .product-card {
    width: auto;
    min-width: 0;
}

/* The catalogue card without its sales copy. On the product page a card has
   a description and a "choose this cover" button because it is asking for a
   decision; here it is only saying "there are more of these", so the cover
   and its name are the whole card and the whole card is the link. */
.cover-card .card-content {
    /* padding: var(--space-3) var(--space-6) */
}

.cover-card .card-content h3,
.confirm-product .card-content h2 {
    font-size: var(--fs-base);
    font-family: var(--font-body);
    font-weight: 400;
    letter-spacing: 1px;

    /* font-family: var(--font-heading-light) */
}

/* Price on the card is for _chart_shop_strip only, where the visitor already
   has a chart and the next question really is what it costs. The homepage
   grid has no prices on purpose: it is answering "how many are there", and a
   wall of prices answers a question nobody asked yet.

   Used by shop.html's catalogue since 2026-09-18, where the groups do not
   all cost the same. Home stays without (Zlatan, same day). */
.cover-card__price {
    font-size: var(--fs-sm);
    font-weight: var(--fw-bold);

    /* color: var(--c-orange-400) */
    color: var(--c-orange-red);
}

/* The shop cards' name plate as a flex row. It held the price beside the
   name for a few hours on 2026-09-18, until Zlatan moved prices to a
   .card-label on the photo (free-chart.html); kept because it does no harm
   and a second item on the plate may come back. */
.catalogue-grid .card-content {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: flex-end;
    column-gap: var(--space-2);
}

.cover-card__price .strike-price {
    font-weight: var(--fw-light);
    color: var(--c-text-soft);
    text-decoration: line-through;
    margin-right: var(--space-1);
}

/* ==========================================================================
   Forms — _form_row, _form_tooltip, _birth_data_form
   ========================================================================== */
/* The other light panel that sits inside a starfield section, so like
   .confirm-card it has to state its own text colour rather than inherit.
   Its labels are `.light-text`, which sets font-WEIGHT only — they always
   took their colour from an ancestor. */
/* The ground comes from .bgr-cream in the markup since 2026-09-25 (Zlatan's
   universal light class), so the two light cards and every light section are
   one colour rather than three near-identical ones. --c-form-gradient is now
   unused; it stays in tokens.css as the record of what this card used to be. */
.chart-form-wrapper {
    text-align: center;
    color: var(--c-text);
    padding-bottom: var(--space-12);
    border-top: 0;
    max-width: var(--max-form-width);
    margin: 0 auto var(--space-6);
    width: 100%;
    border-bottom-left-radius: var(--radius-lg);
    border-bottom-right-radius: var(--radius-lg);
}

.form-content {
    padding-left: 1.25rem;
    padding-right: 1.25rem;
}

.form-intro {
}

/* Both of these are IDs turned into classes, 2026-09-06, same reasoning as
   .proof-grid above: the form band and its blue introduction are the shape of
   this pair of blocks wherever they appear, and the homepage is the second
   page to want them. The IDs stay until product.html is next opened. */
#chart-form-section,
.chart-form-band {
    padding-bottom: var(--space-12);

    /* border-bottom: var(--border-orange-5) */
}

#free-chart-intro,
.order-overlay__intro,
.form-intro-band {
    background-color: var(--c-blue-800);
    box-shadow: var(--shadow-tight);
    background-image: var(--c-blue-gradient-800-rad);
}

#create-new-chart,
.chart-form-intro {
    text-align: center;

    /* color: var(--c-white-soft) */
    text-shadow: var(--shadow-text);

    /* background-color: var(--c-blue-800) */
    /* background-image: var(--c-blue-gradient-900) */
    border-top: var(--border-orange-5);
    box-shadow: var(--shadow-tight);
    background-image: url('../img/icons/astro-star-w15.svg');
    background-repeat: no-repeat;
    background-size: 700px;
    background-position: -400px center;
}

@media (min-width:600px) {
    .chart-form-intro {
        background-size: 600px;
        background-position: -350px center;
    }
}

@media (min-width:768px) {
    .chart-form-intro {
        background-size: 550px;
        background-position: -320px center;
    }
}

@media (min-width:1200px) {
    .chart-form-intro {
        background-size: 590px;
        background-position: -150px -140px;
    }
}

.chart-form-intro h2 {
    color: var(--c-white-85);
}

.chart-form-intro h2 span {
    color: var(--c-orange);
}

.form-wrapper {
    width: 100%;
    margin: 0 auto;
    text-align: left;
}

.form-row {
    padding-bottom: var(--space-4);
    border-bottom: 1px dotted var(--c-black-25);
    margin-bottom: var(--space-3);
}

.w-label {
    display: block;
    margin-bottom: var(--space-3);
}

label.w-row {
    align-items: center;
    gap: var(--space-2);
}

.light-text {
    font-weight: var(--fw-light);
}

.w-input,
.w-select {
    display: inline-block;
    min-height: var(--space-12);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--c-border);
    border-radius: 0;
    font-family: var(--font-body);
    font-weight: var(--fw-light);
    background-color: var(--c-white);
    font-size: var(--fs-lg);
}

.w-input {
    width: 100%;
}

.w-select {
    width: 25%;
    margin-right: 2%;
}

.wide-input {
    width: 32%;
}

select.w-select {
    appearance: none;

    /* The thin chevron, blue on the light form card (2026-09-30: every down
       arrow is one shape). Was img/down.png. */
    background-image: url('../img/icons/chevron-down-blue.svg');
    background-repeat: no-repeat;
    background-position: 90% 50%;
    background-size: 12px;
}

.w-line {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.w-input:focus,
.w-select:focus {
    border: 1px solid var(--c-orange) !important;
    background-color: var(--c-form-input);
}

/* [placeholder] is part of the test: a field with no placeholder attribute is
   never :placeholder-shown, so :not() was true while it was EMPTY and every such
   field showed the green "filled in" outline from the start (the help form,
   2026-10-02). With the attribute required, an empty one stays plain. */
.w-input[placeholder]:not(:placeholder-shown),
.w-select:has(option:checked:not([value=""])) {
    border: 1px solid var(--c-green) !important;
}

.gender-toggle {
    display: flex;
    height: 2.75rem;
    gap: var(--space-3);
}

/* Hidden from sight, NOT display: none (Zlatan, 2026-10-21: "include gender
   group in tabbing"). display: none took the three buttons out of the Tab
   order altogether; clipped to nothing, the radios stay one Tab stop, the
   arrow keys move the choice, and the label shows where the focus is. */
.gender-toggle input[type="radio"] {
    appearance: none;
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

.gender-toggle input[type="radio"]:focus-visible + label {
    outline: 2px solid var(--c-green);
    outline-offset: 2px;
}

.gender-toggle label {
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px solid var(--c-border);
    width: 29%;
    border-radius: var(--radius);
    overflow: hidden;
    transition: var(--transition);
    line-height: 1;
    color: var(--c-border);
    font-size: var(--fs-lg);
}

.gender-toggle label:hover {
    border-color: var(--c-orange);
    background-color: var(--c-white-75);
    cursor: pointer;
    color: var(--c-text);
}

.gender-toggle input[type="radio"]:checked + label {
    background-color: var(--c-white-75);
    border: 1px solid var(--c-green) !important;
    cursor: default;
    color: var(--c-text);
}

.tooltip {
    position: relative;
}

.tooltip .w-label span {
    position: relative;
    font-weight: var(--fw-regular);
    color: var(--c-blue-500);
}

.tooltip .w-label span span {
    display: none;
    position: absolute;
    bottom: 100%;
    left: 0;
    width: 220px;
    margin-bottom: var(--space-2);
    padding: var(--space-3);
    background: var(--c-text);
    color: var(--c-white);
    font-size: .75rem;
    line-height: var(--lh-tight);
    border-radius: var(--radius-sm);
    z-index: 10;
}

.tooltip .w-label span:hover span {
    display: block;
}

/* ---- .info-bubble: a short explanation on hover or focus ----
   The form tooltip's look (dark ground, white 12px, small radius) as a class
   any control can use, with a small arrow under it. Added 2026-10-01 for the
   celebrity wall's size control. The parent positions it and shows it. */
.info-bubble {
    position: absolute;
    bottom: calc(100% + var(--space-2));
    left: 50%;
    z-index: 20;
    width: max-content;
    max-width: 220px;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--c-text);
    color: var(--c-white);
    font-size: var(--fs-xs);
    line-height: var(--lh-tight);
    text-align: center;
    pointer-events: none;
    opacity: 0;
    transform: translate(-50%, 4px);
    transition: opacity var(--transition-fast), transform var(--transition-fast);
}

.info-bubble::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    margin-left: -5px;
    border: 5px solid transparent;
    border-top-color: var(--c-text);
}

/* Below the control instead, for one that sits at the top of the screen —
   the celebrity wall's sticky bar — where a bubble above would be cut off. */
.info-bubble--below {
    top: calc(100% + var(--space-2));
    bottom: auto;
    transform: translate(-50%, -4px);
}

.info-bubble--below::after {
    top: auto;
    bottom: 100%;
    border-top-color: transparent;
    border-bottom-color: var(--c-text);
}

.info-icon {
    width: var(--fs-lg);
    display: inline-block;
    opacity: .5;
}

.info-icon:hover {
    opacity: 1;
}

/* Character limit feedback for a capped field (currently the chart name, 36).
   Deliberately NOT styled like .error-message: a name that has reached the
   limit is valid, it just can't grow, so red would be lying about the state.
   Built by blocks.js from the input's own maxlength. */
.field-meta {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-3);
    margin-top: var(--space-1);
}

.field-notice {
    display: none;
    font-size: var(--fs-xs);
    line-height: var(--lh-tight);
    color: var(--c-blue-500);
    text-align: left;
}

.field-notice.show {
    display: block;
}

.char-counter {
    display: none;
    margin-left: auto;
    font-size: var(--fs-xs);
    line-height: var(--lh-tight);
    color: var(--c-text-soft);
    white-space: nowrap;

    /* Digits shouldn't jiggle the counter's width as the count changes. */
    font-variant-numeric: tabular-nums;
}

.char-counter.show {
    display: block;
}

.char-counter.at-limit {
    color: var(--c-blue-500);
    font-weight: var(--fw-semibold);
}

/* _place_autocomplete — Phase 1a stub over a hardcoded city list; see
   blocks.js. Real data comes from /search_city.json in Phase 1b. */
.place-autocomplete {
    position: relative;
}

.place-suggestions {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 10;
    width: 100%;
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--c-white);
    border: 1px solid var(--c-border);
    border-top: 0;
    max-height: 0;
    opacity: 0;
    overflow: hidden auto;
    visibility: hidden;
    transition: max-height var(--transition), opacity var(--transition), visibility 0s linear var(--transition);
}

.place-suggestions.open {
    max-height: 14rem;
    opacity: 1;
    visibility: visible;
    transition: max-height var(--transition), opacity var(--transition), visibility 0s linear;
}

.place-suggestions li {
    padding: var(--space-3) var(--space-4);
    cursor: pointer;
    font-size: var(--fs-base);
    color: var(--c-text);
}

.place-suggestions li:hover,
.place-suggestions li.active {
    background: var(--c-orange);
    color: var(--c-white-soft);
}

.error-msg {
    max-width: 460px;
    margin: var(--space-2) auto 0;
    padding: var(--space-3) var(--space-4);
    font-size: var(--fs-base);
    color: var(--c-red);
    text-align: center;
    display: none;
}

.error-msg.show {
    display: block;
}

.input-error {
    border-color: var(--c-red) !important;
}

.error-message {
    display: none;
    margin-top: var(--space-1);
    font-size: var(--fs-xs);
    line-height: var(--lh-tight);
    color: var(--c-red);
    clear: both;
}

.error-message.show {
    display: block;
}

.calculate-button {
    background-image: url(../img/astro-chart.png);
    background-position: 1rem 45%;
    background-size: 2.5rem;
    background-repeat: no-repeat;
    padding: var(--space-6) 1.75rem var(--space-6) 4rem;
}

/* ---- The submit button is always live, and always looks it ----
   REVERSED 2026-09-16 (Zlatan): "Remove disabled state. Onclick it should show
   errors if form is not complete."

   From 2026-09-08 the button went grey until the form was valid, green when it
   was, and a hover bubble explained the grey. It was never the HTML `disabled`
   attribute — that would have made it untappable — but it read as disabled,
   which is what Zlatan was objecting to. All three are gone: the grey, the
   green that only existed as its opposite, and the bubble that explained it.

   Nothing about validation changed. A click still runs the full check, marks
   every incomplete field with its own message, shows the line under the form,
   and scrolls to the first field that needs attention.

   .submit-row stays: it is only a relative-positioned wrapper now, and keeping
   it avoids touching the form markup on four pages. */
.submit-row {
    position: relative;
    display: inline-block;
}

.calculate-button:hover {
    background-image: url('../img/checked.webp');
    background-size: 1.65rem;
    background-position: 1.25rem 50%;
}

/* Phones. "Birth chart" made the labels longer (2026-09-21) and a submit
   <input> cannot wrap, so "Calculate this birth chart" ran 38px past the card
   at 375px. Below 600px the button drops its letter-spacing, a size step and
   some of the room around the icon. 359px is the one-off for the smallest
   phones (320px wide), where even that is 5px too wide: one more size step. */
@media (max-width: 599px) {
    .calculate-button {
        padding-right: var(--space-4);
        padding-left: 3.25rem;
        background-position: .75rem 45%;
        background-size: 2.25rem;
        font-size: var(--fs-base);
        letter-spacing: 0;
    }
}

@media (max-width: 359px) {
    .calculate-button {
        font-size: var(--fs-sm);
    }
}

/* ==========================================================================
   _gallery_viewer — full-screen product photos

   Same photos as the inline rotator, shown large. Swipe (or arrow keys) to
   move between them; no pinch-zoom, by decision — the masters are 2100px and
   that is the cap, so there is nothing to zoom into that is not already on
   screen.
   ========================================================================== */
/* 700 — was chosen to clear the cookie banner's 600, and the banner was
   removed 2026-09-06. The value stays: a full-screen viewer the reader opened
   deliberately should be the topmost thing on the page, and 700 is still above
   the overlays (240/250). Kept so the layering has one obvious ceiling.
   The one thing above it since 2026-10-08 is Klaro (notice 999, choices
   1000+, its own values): a consent question must be answerable over
   anything, and it is asked once. */
.gallery-viewer {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 700;
    background: rgb(6 21 34 / 0.94);
    touch-action: pan-y;
}

.gallery-viewer.open {
    display: flex;
    flex-direction: column;
}

.gallery-viewer__track {
    flex: 1;
    display: flex;
    align-items: center;
    min-height: 0;
    transition: transform 320ms var(--ease-viewer, cubic-bezier(.22, .61, .36, 1));
}

.gallery-viewer.is-dragging .gallery-viewer__track {
    transition: none;
}

.gallery-viewer__slide {
    flex: 0 0 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
}

.gallery-viewer__slide img,
.gallery-viewer__slide video {
    /* 2100px is the master width and the cap — never upscale past it, or a
       large screen shows a soft image pretending to be detail. The film is
       shown whole here, uncropped. */
    max-width: min(100%, 2100px);
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--radius-sm);
}

.gallery-viewer__bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-3) var(--space-4);
    color: var(--c-white-soft);
    font-size: var(--fs-sm);
}

/* SUPERSEDED 2026-09-25 by _close_x, which every live page now uses.
   Kept because the two parked archives (product-original.html,
   product-v01.html) still carry this markup, and per the house style a
   rule is not deleted the day one page stops using it. The three image
   files it needs — close-white.svg, close-blue.svg and the 2015
   delete-w-www2015.png — go out with it. */
.gallery-viewer__close {
    background: none;
    border: 0;
    padding: var(--space-2);
    font-size: var(--fs-xl);
    line-height: 1;
    color: var(--c-white-soft);
    cursor: pointer;
}

/* The inline rotator advertises that it opens with a real control, not a
   cursor. A `zoom-in` cursor is invisible until you are already hovering,
   says nothing on touch, and is not a thing anyone looks for any more. */
.image-slider {
    position: relative;
    cursor: pointer;
}

.image-slider__expand {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: rgb(6 21 34 / 0.55);
    color: var(--c-white-soft);
    cursor: pointer;
    opacity: 0;
    transition: opacity var(--transition), background-color var(--transition);
}

.image-slider__expand svg {
    width: 1.1rem;
    height: 1.1rem;
    display: block;
}

.image-slider:hover .image-slider__expand,
.image-slider__expand:focus-visible {
    opacity: 1;
}

.image-slider__expand:hover {
    background: rgb(6 21 34 / 0.8);
}

/* Touch has no hover to reveal it, so it is always there. */
@media (hover: none) {
    .image-slider__expand {
        opacity: 1;
    }
}

/* Prev / next inside the viewer. Swipe covers touch; a mouse needs
   something to aim at. */
.gallery-viewer__nav {
    position: absolute;

    /* --photo-cy is the photo's own centre, measured in JS. Plain 50% would
       be the centre of the whole viewer, which sits lower than the photo
       because the counter/close bar takes height off the top. */
    top: var(--photo-cy, 50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3rem;
    height: 3rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgb(255 255 255 / 0.12);
    color: var(--c-white-soft);
    font-size: var(--fs-xl);
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--transition);
}

.gallery-viewer__nav:hover {
    background: rgb(255 255 255 / 0.25);
}

/* Beside the PHOTO, not at the edge of the screen.

   The photo is centred and its rendered width varies — a square image in a
   wide slide leaves a lot of empty space each side, so arrows pinned to the
   viewport edge end up marooned far away from the thing they control.
   `--photo-w` is measured in JS whenever the slide or the window changes;
   the arrows then hang off the middle by half that width plus a gap. */
.gallery-viewer__nav--prev {
    left: 50%;
    margin-left: calc((var(--photo-w, 60vw) / 2 + var(--space-3)) * -1);
    transform: translate(-100%, -50%);
}

.gallery-viewer__nav--next {
    left: 50%;
    margin-left: calc(var(--photo-w, 60vw) / 2 + var(--space-3));
    transform: translate(0, -50%);
}

/* Hidden on touch, where swipe is the natural idiom and arrows are clutter.

   Also hidden on any narrow screen, mouse or not — decided 2026-08-06 after
   seeing it. The arrows hang off the photo's edge, and once the photo fills
   most of the viewport there is no "beside" left for them to occupy: they end
   up half off-screen. A control that cannot be placed properly is worse than
   no control, and a mouse can still drag the photo across. */
@media (hover: none), (max-width: 767px) {
    .gallery-viewer__nav {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .gallery-viewer__track {
        transition: none;
    }
}

/* ==========================================================================
   Overlays — _modal_overlay, _video_lightbox
   ========================================================================== */
/* .auth-backdrop is a SEPARATE CLASS, not a modifier, and that is on purpose:
   initModalOverlay() finds its backdrop with querySelector('.modal-backdrop'),
   which returns whichever one comes first in the document. Two overlays
   sharing the class would make the privacy overlay's behaviour depend on the
   order the two happen to be written in. Added 2026-09-06. */
.modal-backdrop,
.auth-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 240;
    background: rgba(3, 19, 45, .55);
}

/* stays literal — decided 2026-08-03, not merged into --c-blue-950 */
.modal-backdrop.open,
.auth-backdrop.open {
    display: block;
}

/* .auth-panel shares the shell but not the class, for the same reason as
   .auth-backdrop above — initModalOverlay() takes the first .modal-panel in
   the document. Added 2026-09-06. */
.modal-panel,
.auth-panel {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    top: 5%;
    z-index: 250;
    width: 700px;
    max-width: 92%;
    max-height: 88%;
    margin: 0 auto;
    padding: var(--space-8);
    overflow-y: auto;
    border-radius: var(--radius-sm);
    background-color: var(--c-cream);
    box-shadow: var(--overlay-scrim);
    color: var(--c-text);
}

.modal-panel.open,
.auth-panel.open {
    display: block;
}

.modal-panel h2,
.auth-panel h2 {
    color: var(--c-text);
    font-size: var(--fs-3xl);
}

.modal-panel p,
.modal-panel li {
    font-size: var(--fs-sm);
    line-height: var(--lh-loose);
}

/* SUPERSEDED 2026-09-25 by _close_x, which every live page now uses.
   Kept because the two parked archives (product-original.html,
   product-v01.html) still carry this markup, and per the house style a
   rule is not deleted the day one page stops using it. The three image
   files it needs — close-white.svg, close-blue.svg and the 2015
   delete-w-www2015.png — go out with it. */
.modal-close {
    display: block;
    position: absolute;
    top: 7px;
    right: 7px;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: rgba(176, 181, 178, .6);

    /* stays literal — decided 2026-08-03, no matching token */
    background-image: url(../img/delete-w-www2015.png);
    background-position: 50% 50%;
    background-size: 16px;
    background-repeat: no-repeat;
    cursor: pointer;
}

.video-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(3, 19, 45, .85);
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
}

/* stays literal — decided 2026-08-03 */
.video-modal.open {
    display: flex;
}

.video-modal .video-frame {
    position: relative;
    width: 100%;
    max-width: 940px;
    aspect-ratio: 16/9;
}

.video-modal iframe {
    width: 100%;
    height: 100%;
    border: 0;
}

.video-modal .modal-close {
    top: -44px;
    right: 0;
    background-color: var(--c-white);
}

/* ==========================================================================
   _auth_overlay — sign in / create account / reset password
   Added 2026-09-06 with home.html (block H on the page list).

   One panel and three states rather than three pages, because none of them
   is a destination: you are always in the middle of doing something else
   when you are asked to sign in, and a page navigation loses that.

   It reuses .modal-panel's shell and adds nothing but the widths the form
   needs. The panel is narrower than the privacy overlay it shares a class
   with — a 700px column with two fields in it reads as an empty room.
   ========================================================================== */
.auth-panel {
    width: 460px;
    text-align: left;
}

.auth-lead {
    color: var(--c-text-soft);
    margin-bottom: var(--space-4);
}

/* The form rows come from _birth_data_form, whose divider is right for a
   long form and too busy for two fields. */
.auth-form .form-row {
    border-bottom: 0;
    padding-bottom: 0;
    margin-bottom: var(--space-4);
}

.auth-form .w-label {
    margin-bottom: var(--space-2);
    font-weight: var(--fw-light);
}

.auth-submit {
    width: 100%;
}

.auth-switch {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    text-align: center;
    font-size: var(--fs-sm);
}

/* Rails' answer, above the fields (1b-auth, 2026-10-20): one sentence, chosen
   by the server, never written by the script. An error is red like a field's
   own error (.error-msg); "we sent you a link" is the state's whole content,
   so it reads as body text, not as a warning. */
.auth-message {
    margin-bottom: var(--space-4);
}

.auth-message.is-error {
    color: var(--c-red);
}

/* Google and Facebook (2026-10-21), under the main button after an "or"
   between two short rules. Full width like the main button, outlined so the
   email form stays the first answer; each company's mark sits left of its
   name, the size of a capital. */
.auth-social {
    margin-top: var(--space-4);
}

.auth-or {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
    font-size: var(--fs-sm);
    color: var(--c-text-soft);
}

.auth-or::before,
.auth-or::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--c-border);
}

.auth-social__form {
    margin-bottom: var(--space-2);
}

.auth-social__button {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
}

.auth-social__mark {
    flex: none;
    width: 1.25em;
    height: 1.25em;
}

/* Out of the form since 2026-10-21, so it keeps the form's spacing itself. */
.auth-switch {
    margin-top: var(--space-4);
}

/* The terms line under "Create an account" is small print, like the
   marketing tick box's label (.email-consent): a fact, not a question. */
.auth-terms__text {
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    color: var(--c-text-soft);
}

/* .padding-top-0 used to sit here. Moved to the foot of the file 2026-09-06 —
   see the note there. It is a utility, and a utility that loses to a block
   rule is not a utility. */
/* ==========================================================================
   _order_confirmation — the page between choosing a chart and the cart.

   One layout, three variants (data-confirm-variant="new|saved|edit"). The
   variants differ in the right-hand column and the button, not in the frame,
   so everything here is shared and the variants add almost nothing.
   ========================================================================== */
.confirm-section {
    /* padding: var(--space-12) 0 */
}

/* Which words and button belong to which variant, set by data-confirm-variant
   on <body> (it has to reach the button, which is in a later section than the
   hero). A part lists the variants it belongs to, matched with ~=, as
   data-charts-part does. Added 2026-10-04: a saved chart is not calculated
   again, so it must not say "Calculate". */
[data-confirm-variant="new"] [data-confirm-part]:not([data-confirm-part~="new"]),
[data-confirm-variant="saved"] [data-confirm-part]:not([data-confirm-part~="saved"]),
[data-confirm-variant="edit"] [data-confirm-part]:not([data-confirm-part~="edit"]),
[data-confirm-variant="free"] [data-confirm-part]:not([data-confirm-part~="free"]) {
    display: none;
}

/* Paid by a gift voucher or not (data-gift-state="none|spent", on <body>):
   the same device, for the cover's label and the next stop. */
[data-gift-state="none"] [data-gift-part]:not([data-gift-part~="none"]),
[data-gift-state="spent"] [data-gift-part]:not([data-gift-part~="spent"]) {
    display: none;
}

/* order-confirmation.html carries this as an inline style on its container.
   Made a class 2026-09-06 when free-chart-confirm.html wanted the same
   measurement; that page uses the class, and the inline style stays where it
   is until product-side pages are next opened. */
.confirm-body {
    padding-top: var(--space-16);
}

/* Colour inherited from the enclosing .astro-stars-bg section. */
.confirm-intro {
    max-width: 720px;
    margin: 0 auto var(--space-8);
}

.confirm-heading {
    font-size: var(--fs-4xxl);
    color: var(--c-orange);
    font-weight: var(--fw-semibold);
    letter-spacing: 0;
}

.confirm-lead {
    font-weight: var(--fw-light);
    line-height: var(--lh-snug);
}

/* A light panel inside a dark section, so it states its own text colour.
   It used to get this by inheriting the page default; now that
   .astro-stars-bg sets white, anything relying on inheritance in here would
   be white-on-white. The form labels in variant C were exactly that. */
.confirm-card {
    background-color: var(--c-white-75);
    color: var(--c-text);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
    max-width: var(--container-w);
    margin: 0 auto;
}

.confirm-row {
    gap: 4%;
    align-items: flex-start;
}

/* The free-path confirmation has no product beside the data, so the card is
   one column. --max-form-width is the birth form's own width, which makes
   this card the same column the reader has just finished filling in — the
   data appears where they typed it. Added 2026-09-06. */
.confirm-card--single {
    max-width: var(--max-form-width);
}

/* The one input on that card. It sits below a list of read-only facts, so it
   needs the divider the facts have and none of the long form's row spacing. */
.confirm-email {
    padding-top: var(--space-2);
    padding-bottom: var(--space-4);
    border-bottom: 0;
}

.confirm-email .w-label {
    font-weight: var(--fw-light);
    margin-bottom: var(--space-2);
}

.confirm-email .confirm-note {
    margin-top: var(--space-2);
    text-align: left;
}

/* --- Product side ---
   The cover mockups carry a lot of their own whitespace, so at full column
   width the product side ran ~200px taller than the data beside it and left
   the CTA stranded at the top. Capped so the two columns read as a pair. */
.confirm-product-image {
    max-width: 320px;
    margin: 0 auto var(--space-3);
    position: relative;
}

.confirm-product-image img {
    width: 100%;
    border-radius: var(--radius);
}

.confirm-product-meta {
    text-align: center;
}

.confirm-label {
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    color: var(--c-orange);
    letter-spacing: 1px;
    margin-bottom: var(--space-1);
}

.confirm-product-name {
    font-size: var(--fs-2xl);
    color: var(--c-blue-900);
    text-transform: uppercase;
    font-weight: var(--fw-bold);
    margin-bottom: var(--space-2);
}

.confirm-price-box {
    margin: 0 auto;
}

/* The shared .price-box splits features from price with a left border. There
   are no features here, so that divider would be a line against nothing. */
.confirm-price-box .price-amount {
    border-left: 0;
    margin-left: 0;
    padding-left: 0;
    align-items: center;
}

/* --- Birth data side --- */
.confirm-data-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    border-bottom: 1px solid var(--c-tan);
    padding-bottom: var(--space-2);
    margin-bottom: var(--space-2);
}

.confirm-edit {
    font-size: var(--fs-base);
    font-weight: var(--fw-semibold);

    /* color: var(--c-orange-red) */
    /* text-decoration: underline */
}

.confirm-edit:hover,
.confirm-edit:focus-visible {
    color: var(--c-green);
}

.confirm-saved-badge {
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--c-white-soft);
    background-color: var(--c-green);
    border-radius: var(--radius-pill);
    padding: var(--space-1) var(--space-3);
}

.confirm-list {
    margin: 0 0 var(--space-4);
}

.confirm-item {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--c-cream);
}

.confirm-item dt {
    font-weight: var(--fw-light);
    color: var(--c-text-soft);
    flex: 0 0 auto;
}

.confirm-item dd {
    margin: 0;
    text-align: right;
    font-weight: var(--fw-semibold);
    color: var(--c-blue-900);

    /* The name can run to 36 characters — wrap rather than push the label. */
    overflow-wrap: anywhere;
}

.confirm-actions {
    margin-top: var(--space-6);
    text-align: center;
}

.confirm-button {
    /* width: 100% */
    border: 0;
    cursor: pointer;
    font-family: var(--font-body);
}

.confirm-note {
    /* margin: var(--space-2) 0 0 */
    font-size: var(--fs-sm);
    font-weight: var(--fw-light);
    max-width: var(--max-form-width);
    margin: 0 auto;

    /* color: var(--c-text-soft) */
    line-height: var(--lh-snug);
    text-align: center;
}

.confirm-note a {
    font-size: var(--fs-base);
    font-weight: var(--fw-medium);
}

/* --- Editable variant --- */
.confirm-form .form-row:last-of-type {
    border-bottom: 0;
}

/* ==========================================================================
   _chart_result_hero + _chart_wheel_overlay — the calculated chart
   Added 2026-09-06 (2.3 on the page list).

   Two states of one block, switched by data-chart-state on the section:
   "calculated" (the link has been emailed, the wheel opens here) and
   "ready" (the PDF downloads from here). Everything that differs carries
   data-chart-line or data-chart-action and is hidden by the script — so
   both sentences and both button labels are in the markup.
   ========================================================================== */
.chart-result {
    padding-bottom: var(--space-4);

    /* border-bottom: var(--border-orange-5) */
}

/* A receipt, not a form: the facts run in a line rather than a stacked list,
   because nobody is filling them in and a five-row table here would look
   like something waiting to be edited. */
/* ==========================================================================
   _chart_facts, stacked — the birth data with the labels taken off
   Added 2026-09-08 (Zlatan's layout, for the confirm and result pages).

   THE LABELS WERE DOING WORK FOR THE WRONG PERSON. "Name", "Date of birth",
   "Place of birth" tell a reader nothing that the value beside them does not
   already say. Five label/value pairs is ten things to read when four centred
   lines is four — and this is the one screen whose entire job is that somebody
   actually reads it and spots a wrong minute.

   The row version above stays: it is right where the facts are a receipt
   sitting beside something else. This one is right where they are the subject.
   ========================================================================== */
.chart-facts--stacked {
    display: block;
    max-width: 32em;
    margin: 0 auto var(--space-4);
    padding: 0;
    border: 0;
    text-align: center;
}

.chart-facts--stacked dt {
    margin: 0 auto var(--space-6);
    border-bottom: 1px solid var(--c-tan);
    max-width: var(--max-form-width);
}

.chart-facts--stacked dd {
    margin: 0 0 var(--space-2);
    font-size: var(--fs-2xl);
    font-weight: var(--fw-regular);
    line-height: var(--lh-snug);
}

/* The name is the one line that must be right — it is printed on the cover. */
.chart-fact-name {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-4xl) !important;
    font-weight: var(--fw-semibold) !important;
    margin-top: var(--space-6) !important;
}

/* On the chart page the facts are a receipt, not the thing being checked, so
   the name and the birth lines sit one size smaller than on the confirm step
   (Zlatan, 2026-10-22). */
.chart-facts--view dd {
    font-size: var(--fs-xl);
}

.chart-facts--view .chart-fact-name {
    font-size: var(--fs-3xl) !important;
}

/* Gender gets no row of its own. It is the only field whose value is not
   self-evidently what it is, and it is also the one that matters least. */
.chart-fact-aside {
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    font-weight: var(--fw-light);

    /* color: var(--c-text-soft) */
}

/* THE GENDER SHOWS ONLY WHERE THE BIRTH DETAILS STAND ALONE — the free chart
   confirm and the chart page. Beside a product (the order confirmation, a cart
   line, the saved-chart panel) it is one more thing to read next to the book it
   belongs to, and the book is what the reader is checking. Zlatan, 2026-09-24,
   narrowing a blanket display: none he had put on the class itself.
   It stays in the markup either way: the reading is written from it. */
.chart-facts--order .chart-fact-aside,
.cart-line__person .chart-fact-aside,
.chart-confirm .chart-fact-aside {
    display: none;
}

/* order-confirmation.html puts the book on the same stacked card, above the
   birth details (2026-09-16). The rule between them is the one
   .chart-confirm__product draws in the overlay: two things, bound together,
   read as two. */
.chart-facts--stacked .confirm-product {
    margin-bottom: var(--space-8);

    /* padding-bottom: var(--space-8) */
    border-bottom: 1px dotted var(--c-tan);
}

/* order-confirmation.html from 768px: the book and the birth details side by
   side (Zlatan, 2026-09-18). The card takes the container's width instead of
   the stacked 32em, the arrow spans both columns, and the rule between book
   and details turns from horizontal to vertical. Both classes on the
   selector so it outranks .chart-facts--stacked's display: block. */
@media (min-width: 768px) {
    .chart-facts--stacked.chart-facts--order {
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-items: flex-start;
        max-width: none;
    }

    .chart-facts--order > .section-arrow {
        grid-column: 1 / -1;
    }

    .chart-facts--order .confirm-product {
        margin-bottom: 0;
        padding: 0 var(--space-4) 0 0;
        border-bottom: 0;
        border-right: 1px dotted var(--c-tan);
    }

    .chart-facts--order .order-facts {
        padding-left: var(--space-4);
    }
}

/* Cards waiting to be revealed by initLoadMoreCards(). `display: none`
   rather than a visually-hidden trick, so a hidden card takes no space in the
   grid and the page is honestly short until the reader scrolls for more —
   the same call as .review-card--pending. Added 2026-09-08.

   TWO CLASSES, fixed 2026-09-17: `a.product-card { display: block }` outranks
   a single class, so every "pending" card was quietly on screen from the
   start — on free-chart-view.html's grid too. Found building the shop page. */
.product-card.product-card--pending {
    display: none;
}

/* ==========================================================================
   _chart_confirm_overlay — the product and the chart, together
   Added 2026-09-08.

   Shares .auth-panel's shell (see the note there on why these overlays each
   carry their own class rather than all being .modal-panel). Wider than the
   sign-in panel because it holds two things that have to be compared, not one
   short form.
   ========================================================================== */
.chart-confirm {
    width: 560px;
    text-align: center;
}

.chart-confirm__product {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    text-align: left;
    margin-bottom: var(--space-6);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--c-tan);
}

.chart-confirm__product img {
    flex: 0 0 auto;
    width: 96px;
    border-radius: var(--radius-sm);
}

.chart-confirm__name {
    font-family: var(--font-heading);
    font-size: var(--fs-xl);
}

/* .chart-facts--stacked is white-on-blue where it was born; here it sits on
   the cream panel, so the muted colour has to be restated. */
.chart-confirm .chart-fact-aside {
    color: var(--c-text-soft);
}

@media (max-width: 600px) {
    .chart-confirm__product {
        /* flex-direction: column */
        /* text-align: center */
    }
}

/* ==========================================================================
   _chart_signs — sun, moon and rising, on the result page
   Added 2026-09-08 (Zlatan).

   Bible §5: the largest non-brand search cluster we have is people looking up
   their rising sign — around 3,800 clicks a year — and "the traffic arrives
   with tool intent. People come to look something up; we sell them a gift."
   This is where the looking-up is answered, and it is the first thing on the
   page that could not have existed before the chart was calculated.

   It is also the argument for the book with no selling in it: three lines
   here, forty pages there.
   ========================================================================== */
.chart-signs {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: var(--space-4);
    max-width: 32em;
    margin: var(--space-8) auto 0;
    padding: 0;
}

.chart-sign {
    flex: 1 1 8em;
    padding: var(--space-4) var(--space-2);
    border-radius: var(--radius);
    background-color: var(--c-white-75);

    /* border: 1px solid var(--c-blue-700) */
    text-align: center;
}

.chart-sign__label {
    display: block;
    font-size: var(--fs-base);
    font-weight: var(--fw-bold);
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--c-orange);
    margin-bottom: var(--space-1);
}

.chart-sign__value {
    display: block;
    font-family: var(--font-heading);
    font-size: var(--fs-xl);
    line-height: var(--lh-tight);
    margin-bottom: 0.5rem;
}

/* SHOWN ONLY WHEN THE TIME WAS ASSUMED. The rising sign is the one placement
   that turns on the exact minute, so with a guessed noon it is not a fact and
   must not be printed as one. Rails sets data-time-unknown from the flag the
   form captures; hidden by default, because the honest default is that we
   were given a real time. */
.chart-signs__caveat {
    display: none;
    max-width: 32em;
    margin: 0 auto 0;
    font-size: var(--fs-sm);

    /* color: var(--c-white-75) */
}

.chart-signs[data-time-unknown="1"] + .chart-signs__caveat {
    display: block;
}

/* With a real birth time there is no caveat, so the block below needs the
   space the caveat would have taken. */
.chart-signs:not([data-time-unknown="1"]) {
    margin-bottom: var(--space-6);
}

/* _free_reading (1b-chart-view, 2026-10-22): the short reading under the
   three signs, one text each for the Sun, the Moon and the Ascendant. The
   texts are Marsa's, from the database, and some open with their own <h2>
   headline, which is kept a step smaller than the placement's title. */
.free-reading__part {
    margin-top: var(--space-6);
}

.free-reading__title {
    font-size: var(--fs-xl);
    margin-bottom: var(--space-2);
}

.free-reading__part h2:not(.free-reading__title) {
    font-size: var(--fs-lg);
    margin-bottom: var(--space-2);
}

.confirm-edit-row {
    text-align: center;
    margin-bottom: var(--space-8);
}

/* The light band the facts sit on. The rest of this page is the starfield, so
   this is the only light thing on it and the eye goes straight there. */
.confirm-facts {
    padding-bottom: var(--space-12);

    /* background-color: var(--c-cream) */
    /* background-image: var(--c-white-gradient-cream) */
}

.confirm-hero {
    padding-top: var(--space-12);
    padding-bottom: var(--space-8);
}

/* ==========================================================================
   _email_capture — two states, data-email-state
   Added 2026-09-08.

   `new`   we do not know this person — an input, plus a way to sign in
   `known` we do — the address is SHOWN, not asked for

   Asking a signed-in customer to retype an address we already hold is how a
   nearly-finished form gets abandoned. Pure CSS: the state attribute decides
   which half exists, so nothing here needs a script to render correctly, and
   Rails sets the attribute from whether there is a session.
   ========================================================================== */
.email-capture {
    max-width: var(--max-form-width);
    margin: var(--space-4) auto 0;
    padding-top: var(--space-6);
    text-align: center;
    border-top: 1px solid var(--c-cream);
}

.email-question {
    font-family: var(--font-heading);
    font-size: var(--fs-lg);
    margin-bottom: var(--space-4);
}

.email-address {
    font-size: var(--fs-lg);
    margin-bottom: var(--space-1);
}

.email-signin {
    font-size: var(--fs-base);
    font-weight: 400;

    /* color: var(--c-text-soft) */
}

/* The marketing tick box under the address (2026-10-02). Left-aligned text
   beside the box, centred as a block like the rest of the capture; the label
   states its own size so it reads as a line of small print, not a question. */
.email-consent {
    max-width: 30rem;
    margin: 0 auto var(--space-4);
    text-align: left;
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
}

.email-consent input {
    flex: none;
    margin-top: .2em;
}

.email-capture[data-email-state="new"] [data-email-part="known"],
.email-capture[data-email-state="known"] [data-email-part="new"] {
    display: none;
}

/* _confirm_step — details first, then the address. Added 2026-09-16 (Zlatan).
   One question at a time: the reader confirms the chart before being asked
   where to send it, so the email field never competes with the check.
   `check` hides the email half; `email` hides the button. The button fades
   (.is-leaving, set by initConfirmStep) and the address then unveils top to
   bottom, like a page being pulled down. */
[data-step-state="check"] [data-step-part="email"],
[data-step-state="email"] [data-step-part="check"] {
    display: none;
}

[data-step-part="check"] {
    transition: opacity .2s ease-out;
}

[data-step-part="check"].is-leaving {
    opacity: 0;
}

[data-step-state="email"] [data-step-part="email"] {
    animation: step-unveil .7s ease-out backwards;
}

@keyframes step-unveil {
    from {
        clip-path: inset(0 0 100% 0);
    }

    to {
        clip-path: inset(0 0 0 0);
    }
}

@media (prefers-reduced-motion: reduce) {
    [data-step-part="check"] {
        transition: none;
    }

    [data-step-state="email"] [data-step-part="email"] {
        animation: none;
    }
}

/* A label the eye does not need and a screen reader does. The input carries a
   placeholder and sits under a heading that asks the question in words, so a
   visible "Email address" above it would be the third time of asking. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.chart-facts {
    /* display: flex */
    /* flex-wrap: wrap */
    /* justify-content: center */
    /* gap: var(--space-4) var(--space-8) */
    max-width: 720px;
    margin: 0 auto var(--space-6);
    padding: 0 var(--space-4) var(--space-12);

    /* background-color: var(--c-white-soft) */
    color: var(--c-text);

    /* background-image: var(--c-white-gradient-cream) */
    border-bottom-left-radius: var(--radius-lg);
    border-bottom-right-radius: var(--radius-lg);
    position: relative;
    overflow: hidden;

    /* border-top: 1px solid var(--c-blue-950) */
    /* border-bottom: 1px solid var(--c-blue-950) */
}

.chart-fact dt {
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--c-orange);
    margin-bottom: var(--space-1);
}

.chart-fact dd {
    margin: 0;
    font-weight: var(--fw-light);
}

.chart-actions {
    text-align: center;
}

/* The two states' controls occupy the same slot, and only one is ever
   shown — so the hidden one must not reserve a line box. `[hidden]` is
   honoured by the UA stylesheet, but `.button` sets display:inline-block,
   which is more specific than `[hidden]`'s display:none and overrides it. */
.chart-action[hidden] {
    display: none;
}

/* The wheel overlay. Wider than _auth_overlay, which is a two-field form;
   this one is a square drawing and wants to be as large as the panel's own
   92% ceiling allows. */
.wheel-panel {
    width: 100%;
    padding: var(--space-12) var(--space-4);
    max-width: 100%;
    height: 100vh;
    max-height: 100vh;
    top: 0;
    bottom: 0;
    text-align: center;
    background-image: var(--c-white-gradient-cream);
}

/* White, not cream, and not the night blue this first tried. The wheel is a
   black line drawing — a document, not a picture of the sky — and it is read
   the way a diagram is read. Paper is the right ground for it, and the plate
   also lifts it off .auth-panel's cream so it does not look like an
   illustration printed on the panel.

   (The first attempt used a dark plate to rescue img/astro-chart.svg, which
   is stroked in white and rendered as nothing on cream. That was solving the
   wrong problem — the file was the wrong drawing. See the note in
   free-chart-view.html.) */
.wheel-plate {
    /* background-color: var(--c-white) */
    /* border-radius: var(--radius-lg) */
    /* padding: var(--space-1) */
    margin: 0 -1rem var(--space-6) -1rem;
}

.wheel-drawing {
    width: 100%;
    max-width: 940px;
    height: auto;
    margin: 0 auto;
}

/* Opening, 2026-09-16 (Zlatan). The panel fades in, then the wheel turns into
   place — the sky settling into the position it held at the moment of birth.
   It ends at rotate(0), so the drawing is read exactly as generated; this
   moves the picture, never the chart. Plays on .open because display:none ->
   block restarts a CSS animation. Closing stays instant: a reader who has
   finished looking wants the page back, not a second show. */
.wheel-panel.open {
    animation: wheel-panel-enter .35s ease-out backwards;
}

.wheel-panel.open .wheel-drawing {
    animation: wheel-turn-in 1.2s cubic-bezier(.2, .7, .2, 1) .15s backwards;
}

@keyframes wheel-panel-enter {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

@keyframes wheel-turn-in {
    from {
        opacity: 0;
        transform: rotate(-90deg) scale(.8);
    }

    to {
        opacity: 1;
        transform: rotate(0) scale(1);
    }
}

/* Planets fall from the sky, 2026-10-14 (Zlatan: "make them like they fall
   from the sky", one by one). After the wheel has turned in, each planet
   drops into its place, Sun to Pluto, then the points; the pointer lines
   fade in last, once everything they point from has landed. initWheelFall()
   places the SVG in the page (an <img> cannot move its parts) and wraps each
   symbol in a .wheel-fall group carrying its place in the order, --fall-i.
   The group moves, not the <path>: a CSS transform would replace the path's
   own translate/scale attribute and throw the symbol to the corner.

   Starts at .5s (Zlatan, 2026-10-14: first 1.2s, then .9s "just before the
   first animation finishes", then .7s, then .5s). The turn-in runs .15s to 1.35s, but
   its ease-out has it nearly still by about 1s, so at 1.2s the first planet
   only showed once the wheel looked finished. At .5s the Sun falls while the
   wheel is still visibly turning.
   90ms apart: fourteen symbols land in about two seconds. The fall is
   420 units of the 1224 drawing, about a third: from above the wheel's top
   edge for the planets on the upper half. Gravity is the ease-in to 60%, then
   one small bounce. backwards keeps each symbol hidden until its turn. */
.wheel-panel.open .wheel-fall {
    animation: wheel-fall-in .8s calc(.5s + var(--fall-i, 0) * 90ms) backwards;
}

.wheel-panel.open .wheel-fall-late {
    animation: wheel-panel-enter .5s ease-out calc(.5s + var(--fall-n, 0) * 90ms + .5s) backwards;
}

@keyframes wheel-fall-in {
    0% {
        opacity: 0;
        transform: translateY(-420px);
        animation-timing-function: cubic-bezier(.5, 0, 1, 1);
    }

    60% {
        opacity: 1;
        transform: translateY(0);
        animation-timing-function: ease-out;
    }

    78% {
        transform: translateY(-14px);
        animation-timing-function: ease-in;
    }

    100% {
        opacity: 1;
        transform: translateY(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .wheel-panel.open,
    .wheel-panel.open .wheel-drawing,
    .wheel-panel.open .wheel-fall,
    .wheel-panel.open .wheel-fall-late {
        animation: none;
    }
}

/* ---- zoom and move, 2026-10-01 ----
   Zlatan: "user can open chart in overlay, full page, magnifier and mover",
   then: "when zoomed make it visible full screen, under the name and
   controls". So the STAGE is the whole screen, always, behind everything:
   at 100% the wheel sits in the space between the name (top) and the tools
   (bottom); zoomed, it runs under both, and they float over it on a soft
   fade of the panel's own paper. The ZOOM layer inside it is what moves
   (translate + scale from its top-left corner) — not the <img>, which
   carries the turn-in animation; two transforms on one element would fight.
   touch-action: none hands pinch and drag to the script.

   --wheel-top / --wheel-bottom are the room the head and the foot take;
   the panel with the view switch (the chart pages) needs more on top. */
.wheel-panel {
    --wheel-top: 9.5rem;
    --wheel-bottom: 8.5rem;
}

.wheel-panel:has(.wheel-views) {
    --wheel-top: 12.5rem;
}

.wheel-panel .close-x {
    position: fixed;
    z-index: 4;
}

/* The head: name, birth line, view switch. Above the stage; it lets drags
   through except on its buttons. */
.wheel-head {
    position: relative;
    z-index: 2;
    pointer-events: none;
}

.wheel-head button {
    pointer-events: auto;
}

.wheel-stage {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: clip;
    touch-action: none;
    cursor: zoom-in;
}

.wheel-stage[data-zoom="max"] {
    cursor: zoom-out;
}

.wheel-stage.is-dragging {
    cursor: grabbing;
}

.wheel-zoom {
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: var(--wheel-top) var(--space-4) var(--wheel-bottom);
    transform-origin: 0 0;
}

/* Animated only for the buttons and a click; a drag or a pinch follows the
   finger with no lag. The script sets .is-easing for the stepped moves. */
.wheel-zoom.is-easing {
    transition: transform .2s ease-out;
}

.wheel-stage .wheel-drawing {
    display: block;
    width: 100%;
    height: 100%;
    max-width: none;
    object-fit: contain;
    -webkit-user-drag: none;
    user-select: none;
}

/* The fades the head and the foot float on: the panel's own paper, so a
   zoomed wheel disappears under them instead of colliding with the type. */
.wheel-plate::before,
.wheel-plate::after {
    content: "";
    position: fixed;
    left: 0;
    right: 0;
    z-index: 1;
    pointer-events: none;
}

.wheel-plate::before {
    top: 0;
    height: var(--wheel-top);
    background-image: linear-gradient(to bottom, var(--c-white-soft) 65%, transparent);
}

.wheel-plate::after {
    bottom: 0;
    height: var(--wheel-bottom);
    background-image: linear-gradient(to top, var(--c-cream) 60%, transparent);
}

/* The foot: the tools and the hint, fixed at the bottom over the stage. */
.wheel-foot {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    padding: 0 var(--space-4) var(--space-6);
    pointer-events: none;
}

.wheel-foot button {
    pointer-events: auto;
}

/* The tools: zoom out, the level, zoom in, fit. Round outlined buttons on
   the light panel, the close's own ring and colour. */
.wheel-tools {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
}

.wheel-tool {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 1px solid var(--c-border);
    border-radius: 50%;
    background-color: var(--c-white-soft);
    color: var(--c-text);
    font-family: var(--font-body);
    font-size: var(--fs-xl);
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}

.wheel-tool:disabled {
    opacity: .35;
    cursor: default;
}

@media (hover: hover) {
    .wheel-tool:not(:disabled):hover {
        border-color: var(--c-green);
        background-color: var(--c-green);
        color: var(--c-white);
    }
}

.wheel-tool:focus-visible {
    outline: 2px solid var(--c-orange);
    outline-offset: 2px;
}

.wheel-zoom-level {
    min-width: 3.5rem;
    font-size: var(--fs-sm);
    font-variant-numeric: tabular-nums;
    color: var(--c-text-soft);
}

.wheel-hint {
    margin: var(--space-2) 0 0;
    font-size: var(--fs-sm);
    color: var(--c-text-soft);
}

/* ---- the view switch: Chart | Positions and aspects ----
   Only on panels that have both (the chart pages). data-wheel-view on the
   panel says which shows; each half is marked data-wheel-view-part. */
.wheel-views {
    display: inline-flex;
    gap: 0;
    margin-top: var(--space-2);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    overflow: clip;
}

.wheel-view {
    padding: var(--space-2) var(--space-4);
    border: 0;
    background: var(--c-white-soft);
    color: var(--c-text);
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    letter-spacing: 1px;
    text-transform: uppercase;
    cursor: pointer;
    transition: background-color var(--transition-fast), color var(--transition-fast);
}

.wheel-view[aria-pressed="true"] {
    background-color: var(--c-blue-800);
    color: var(--c-white);
}

@media (hover: hover) {
    .wheel-view[aria-pressed="false"]:hover {
        background-color: var(--c-cream);
    }
}

.wheel-panel[data-wheel-view="chart"] [data-wheel-view-part="table"],
.wheel-panel[data-wheel-view="table"] [data-wheel-view-part="chart"] {
    display: none;
}

/* ---- _chart_sheet: positions and aspects (2026-10-01) ----
   Zlatan: "check how aspects table is built [on the live chart page] …
   even better example is in full reading PDF". The PDF's chart page, as
   one sheet: each planet with its sign, exact position and house, then the
   aspect grid as a triangle — the planet's own glyph on the diagonal, each
   aspect where two rows meet, coloured by kind. The house cusps sit in
   the two angle rows at the bottom of the grid, as on the live chart page
   (2026-10-21; they were a table of their own). Under it the points and
   the elements by mode. Lines in the PDF's #d9d9d9. In the grid every
   cell that is or could be filled is white; only the triangle past the
   diagonal is clear, the panel's cream showing through (2026-10-21). The
   Points and Elements tables stay white. */
.chart-sheet {
    position: relative;
    z-index: 2;
    max-width: 52rem;
    margin: var(--space-6) auto 0;
    text-align: left;
}

/* The table may be wider than a phone: it scrolls sideways in its own box,
   never the page. */
.chart-sheet__scroll {
    overflow-x: auto;
    margin: 0 calc(var(--space-4) * -1) var(--space-8);
    padding: 0 var(--space-4);
}

.chart-sheet__heading {
    margin: 0 0 var(--space-3);
    font-family: var(--font-body);
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--c-orange-400);
}

.aspect-table {
    border-collapse: collapse;
    font-size: var(--fs-sm);
    color: var(--c-text);
}

.aspect-table td,
.aspect-table th {
    height: 2.25rem;
    padding: 0;
    border: 1px solid var(--c-line-light);
    text-align: center;
    vertical-align: middle;
    font-weight: var(--fw-regular);
}

.aspect-table .astro-glyph {
    font-size: var(--fs-md);
}

.aspect-table :where(td, th):not(.aspect-table__past) {
    background-color: var(--c-white);
}

/* The planet's own part of the row: glyph, "Sun in Scorpio", position,
   house — set off from the grid by a heavier line, as in the PDF. */
.aspect-table__glyph {
    width: 2.25rem;
}

.aspect-table__glyph .astro-glyph {
    font-size: var(--fs-lg);
    color: var(--c-orange-400);
}

.aspect-table__name {
    padding: 0 var(--space-3) !important;
    text-align: left !important;
    white-space: nowrap;
}

.aspect-table__pos {
    padding: 0 var(--space-3) !important;
    text-align: right !important;
    white-space: nowrap;
}

.aspect-table__house {
    width: 2.25rem;
    border-right: 2px solid var(--c-text-soft) !important;
    color: var(--c-text-soft);
}

/* The grid: square cells, the diagonal holds the row's planet, cells past
   it are borderless and clear (the triangle), empty cells stay white. */
.aspect-table__cell {
    width: 2.25rem;
    min-width: 2.25rem;
}

.aspect-table__self .astro-glyph {
    color: var(--c-text-soft);
}

.aspect-table__past {
    border: 0 !important;
    background-color: transparent;
}

.aspect--conjunction {
    color: var(--c-aspect-conjunction);
}

.aspect--sextile {
    color: var(--c-aspect-sextile);
}

.aspect--square {
    color: var(--c-aspect-square);
}

.aspect--trine {
    color: var(--c-aspect-trine);
}

.aspect--inconjunct {
    color: var(--c-aspect-inconjunct);
}

.aspect--opposition {
    color: var(--c-aspect-opposition);
}

/* Phone: the grid is the point, so the row's words step aside — the glyph
   and the position say which planet and where — the cells shrink, and the
   glyph column stays pinned while the grid scrolls sideways under it.
   The name column folds to nothing rather than display: none, so the
   angle rows' cusps (colspan 3, across name, position, house) still line
   up with the grid; pos drops its left line so the fold leaves one line,
   not two. */
@media (max-width: 599px) {
    .aspect-table__name {
        width: 0;
        max-width: 0;
        padding: 0 !important;
        overflow: hidden;
        font-size: 0;
        border-left: 0 !important;
        border-right: 0 !important;
    }

    .aspect-table__pos {
        border-left: 0 !important;
    }

    .aspect-table td,
    .aspect-table th {
        height: 1.875rem;
    }

    .aspect-table__cell,
    .aspect-table__glyph,
    .aspect-table__house {
        width: 1.875rem;
        min-width: 1.875rem;
    }

    .aspect-table__pos {
        padding: 0 var(--space-2) !important;
    }

    .aspect-table__glyph {
        position: sticky;
        left: 0;
        z-index: 1;
    }
}

/* The two angle rows hold the house cusps instead of name, position and
   house: the angle's own position (the 1st or 10th cusp), then the next
   two, each after its number. */
.aspect-table__cusps {
    padding: 0 var(--space-3) !important;
    border-right: 2px solid var(--c-text-soft) !important;
    text-align: left !important;
    white-space: nowrap;
}

.aspect-table__cusp + .aspect-table__cusp {
    margin-left: var(--space-4);
}

.aspect-table__cusp-num {
    margin-right: var(--space-2);
    font-weight: var(--fw-bold);
    color: var(--c-text-soft);
}

/* Phone: stacked, one cusp a line, right-aligned like the positions above.
   After the rule above, or its !important wins on source order. */
@media (max-width: 599px) {
    .aspect-table__cusps {
        padding: var(--space-1) var(--space-2) !important;
        text-align: right !important;
    }

    .aspect-table__cusp {
        display: block;
    }

    .aspect-table__cusp + .aspect-table__cusp {
        margin-left: 0;
    }
}

/* ---- the two small tables under the grid (three until the houses moved
   into the grid, 2026-10-21) ---- */
.chart-sheet__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-8);
    margin-bottom: var(--space-8);
}

@media (min-width: 768px) {
    .chart-sheet__row {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.sheet-table {
    width: 100%;
    border-collapse: collapse;
    background-color: var(--c-white);
    font-size: var(--fs-sm);
    color: var(--c-text);
}

.sheet-table th,
.sheet-table td {
    height: 2rem;
    padding: 0 var(--space-2);
    border: 1px solid var(--c-line-light);
    text-align: left;
    vertical-align: middle;
    font-weight: var(--fw-regular);
    white-space: nowrap;
}

.sheet-table .astro-glyph {
    font-size: var(--fs-md);
}

/* The PDF's grey number cells for the houses. */
.sheet-table__num {
    width: 2rem;
    background-color: var(--c-cream-light);
    font-weight: var(--fw-bold) !important;
    text-align: center !important;
}

.sheet-table thead th {
    border: 0;
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--c-text-soft);
    text-align: center;
}

.sheet-table--elements td {
    text-align: center;
    letter-spacing: 2px;
}

.sheet-table--elements tbody th {
    font-weight: var(--fw-semibold);
}

/* The legend: each aspect's glyph in its colour and its name. */
.aspect-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-6);
    margin: 0 0 var(--space-12);
    padding: 0;
    list-style: none;
    font-size: var(--fs-sm);
    color: var(--c-text);
}

.aspect-legend .astro-glyph {
    margin-right: var(--space-1);
    font-size: var(--fs-md);
}

/* Stack on phones. The product column leads, so the reader sees what they are
   buying before the data they are checking. */
@media (max-width: 767px) {
    .confirm-card {
        padding: var(--space-4);
    }

    .confirm-product {
        margin-bottom: var(--space-4);
        padding-bottom: var(--space-6);
    }
}

/* ==========================================================================
   _shop_intro — the book, the reading and the astrologer, side by side
   Added 2026-09-17 for shop.html (3.1 on the page list).

   One column on a phone, three from 768px. Three short answers to three
   separate doubts, so they stand as equals rather than as a story. The
   pictures are square and on no radius, like the cards below them.
   ========================================================================== */
.shop-intro-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-12);
    text-align: center;
}

@media (min-width: 768px) {
    .shop-intro-grid {
        grid-template-columns: repeat(3, 1fr);
        gap: var(--space-8);
    }
}

.shop-intro-item img {
    display: block;
    width: 100%;
    max-width: 16rem;
    height: auto;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    margin: 0 auto var(--space-6);
    background-color: var(--c-cream);
    border-radius: var(--radius);
}

.shop-intro-item .book-price {
    margin-top: 0;
}

/* Stacked on a phone, the three items are divided by a dotted orange line
   (Zlatan, 2026-09-28), the same line as the calculation note's. Moved here
   from the utilities at the end of the file: it is this block's own rule. */
@media (max-width: 767px) {
    .shop-intro-item {
        border-bottom: 2px dotted var(--c-blue-800);
        padding-bottom: var(--space-12);
    }

    .shop-intro-item:last-child {
        border-bottom: none;
        padding-bottom: 0;
    }
}

.shop-intro-item h3,
.reading-detail-row h3,
#account-offer h3 {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-xl);
}

.order-recap-section h2.dark-heading {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-4xl);
}

/* ==========================================================================
   _reading_detail — what is in the reading, below the shop grid
   Added 2026-09-18. Two columns from 768px (.row / .col-48 stack below it),
   then the inside pages: 2 across on a phone, 4 from 768px. The pages are
   white paper, so they sit on the dark ground as photos do — the one light
   thing in the section.
   ========================================================================== */
.reading-detail-row {
    column-gap: 4%;
    margin-bottom: var(--space-8);

    /* text-align: left */
}

/* The list is followed by "How it is calculated" in the same column at every
   width, so the space under it is the same on a desktop too (Zlatan,
   2026-10-22: it was phone-only, and on a desktop the heading sat on the
   list). */
.reading-detail-row ul.feature-list {
    margin-bottom: var(--space-8);
}

.inside-pages {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-3);
    margin-bottom: var(--space-8);
}

@media (min-width: 768px) {
    .inside-pages {
        grid-template-columns: repeat(4, 1fr);
    }
}

.inside-pages img {
    display: block;
    width: 100%;
    height: auto;
    box-shadow: var(--shadow-soft);
}

/* ==========================================================================
   _catalogue — every product, with filter, sort and load-on-scroll
   Added 2026-09-17 for shop.html. Driven by initCatalogue().

   The grid is even on purpose: 1, 2 (from 600px), then 3 (from 900px)
   across. A filtered set can be any length, and only an even grid reads
   the same at every length.
   ========================================================================== */
.catalogue-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-8);
}

.catalogue-filter {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Square, outlined, uppercase — the section-label voice as a control. The
   current choice is filled orange, the one accent on the bar. Shared with the
   product gallery's Photos / 3D switch, which is the same kind of choice. */
.catalogue-filter a,
.gallery-switch__button {
    display: inline-block;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--c-white-55);
    color: var(--c-white-85);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    letter-spacing: 1px;
    text-transform: uppercase;
    text-decoration: none;
    transition: var(--transition);
}

@media (min-width:600px) {
    .catalogue-filter a,
    .gallery-switch__button {
        padding: var(--space-2) var(--space-4);
    }
}

.catalogue-filter a:hover,
.catalogue-filter a:focus-visible,
.gallery-switch__button:hover,
.gallery-switch__button:focus-visible {
    border-color: var(--c-orange);
    color: var(--c-orange);
}

.catalogue-filter a[aria-current="true"],
.gallery-switch__button[aria-pressed="true"] {
    border-color: var(--c-orange);
    background-color: var(--c-orange);
    color: var(--c-blue-950);
}

.catalogue-sort select {
    padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3);
    border: 1px solid var(--c-white-55);
    border-radius: 0;
    color: var(--c-white-85);
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    background-color: transparent;
    background-image: url('../img/icons/chevron-down-white.svg');
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    background-size: .75rem;
    appearance: none;
    cursor: pointer;
}

/* The open list is drawn by the system, on white, so its options need the
   dark text back. */
.catalogue-sort option {
    color: var(--c-text);
}

.catalogue-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-4);
}

@media (min-width: 600px) {
    .catalogue-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 900px) {
    .catalogue-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* Same hand-back as .cover-grid: a.product-card brings carousel widths. */
.catalogue-grid > .product-card {
    width: auto;
    min-width: 0;
}

/* a.product-card sets display: block, which beats the browser's own rule
   for [hidden] — so a filtered-out card would stay on screen without this. */
.catalogue-grid > .product-card[hidden] {
    display: none;
}

/* Every card square, whatever the file. The book photographs already are;
   a placeholder that is not (the dog book's empty cover) is shown whole on
   the cream card instead of cropped. */
.catalogue-grid .card-image img {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: contain;
}

.catalogue-empty {
    margin: var(--space-8) 0 0;
    color: var(--c-white-75);
}

/* ==========================================================================
   _catalogue, short row — centred (2026-10-08)
   Used by shop.html's dog books and product-dog.html's other covers.

   The books' card size at every width (1, 2, then 3 across), but from
   900px the columns are fixed thirds and auto-fit drops the empty ones,
   so a row of two sits in the middle instead of leaving a hole on the
   right. No count is written: three cards fill the row, four start a
   second one.
   ========================================================================== */
@media (min-width: 900px) {
    .catalogue-grid--centred {
        grid-template-columns: repeat(auto-fit, calc((100% - 2 * var(--space-4)) / 3));
        justify-content: center;
    }
}

/* ==========================================================================
   _product_gallery — photos, or the 3D book, in one frame
   UNUSED since 2026-09-18: the 3D book went back inline in #book-show and
   opens full screen elsewhere (_book3d_overlay). Kept per the house style.
   Added 2026-09-17 (page list 3.2). Driven by initProductGallery().

   data-gallery-state decides which half exists, the same shape as
   data-email-state: pure CSS, so the page renders correctly before any
   script runs.
   ========================================================================== */
.product-gallery[data-gallery-state="photos"] [data-gallery-part="3d"],
.product-gallery[data-gallery-state="3d"] .image-slider {
    display: none;
}

/* The light ground the book stood on in #book-show, brought with it. The
   ground shadow spreads to 1.28x a cover, past the column, so it is clipped
   here — outside the 3D context, never inside it (house style 2026-09-09). */
.gallery-3d {
    padding: var(--space-6) 0 var(--space-4);
    background-image: var(--c-white-gradient-cream);
    overflow-x: clip;
}

/* --in is book3d.css's one scale knob. There it is sized to the whole
   container; here the book shares a row with the buying column, so it is
   sized to that column instead: .col-48 of .w-container's content box, which
   is min(100vw, --container-w) less .w-container's 0.75em sides (24px at the
   root size book3d.css already assumes). 0.9 leaves air round the book on
   the light ground, as the photos have their own frame. Below 768px the
   columns stack and the column is the full width, so the factor is 1. */
.gallery-3d .book3d {
    --gallery-col: 1;
    --in: calc(min(80vh, 92vw, (min(100vw, var(--container-w)) - 24px) * var(--gallery-col)) * .9 / 8.25);
}

@media (min-width: 768px) {
    .gallery-3d .book3d {
        --gallery-col: .48;
    }
}

.gallery-switch {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

.gallery-switch__button {
    background-color: transparent;
    font-family: var(--font-body);
    cursor: pointer;
}

/* ==========================================================================
   _order_overlay — as a panel, from 2026-09-21
   Zlatan: "redesign using #auth-overlay as a template". It is .auth-panel's
   shell (cream, centred, scrolls inside itself) plus .order-panel: wide
   enough for the birth form, and one layer DOWN, so the saved-chart confirm
   and the privacy note that open from inside it (240/250) land on top and
   dim it. The form's light card is dropped — it would be cream on cream.
   ========================================================================== */
.auth-panel.order-panel {
    z-index: 235;
    width: 600px;
}

.auth-backdrop[data-order-backdrop] {
    z-index: 232;
}

.order-panel .chart-confirm__product {
    margin-bottom: var(--space-6);
}

.order-panel .form-content {
    padding-left: 0;
    padding-right: 0;
}

/* ==========================================================================
   _order_overlay — "Make your book", over the product page
   UNUSED since 2026-09-21: the full-screen version below was replaced by the
   panel above. Kept per the house style.
   Added 2026-09-17 (page list 3.3). Driven by initOrderOverlay().

   Full screen and scrolling on its own, so a long form on a phone is a page,
   not a box. z-index 230: above the floating bar (200), BELOW the backdrops
   (240) and panels (250) of the overlays that open from inside it — the
   saved-chart confirm and the privacy note — so those land on top.
   ========================================================================== */
.order-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 230;
    overflow-y: auto;
    overscroll-behavior: contain;
    background-color: var(--c-blue-900);
    background-image: var(--c-blue-gradient-950);
}

.order-overlay.open {
    display: block;
}

/* Room above the cover row for the close button, now that there is no
   heading band to carry it (simplified 2026-09-18). */
.order-overlay__form {
    padding-top: var(--space-16);
}

.order-overlay__form .chart-confirm__product {
    margin-bottom: var(--space-4);
}

/* The heading text, back on the plain overlay ground (2026-09-18). The
   heading's orange <span> comes from .chart-form-intro on the page, which is
   not here, so it is restated for the overlay. */
.order-overlay__heading {
    margin-bottom: var(--space-6);
}

.order-overlay__heading h2 span {
    color: var(--c-orange);
}

/* The round outlined close from the floating bar, sized up for a page. */
/* SUPERSEDED 2026-09-25 by _close_x, which every live page now uses.
   Kept because the two parked archives (product-original.html,
   product-v01.html) still carry this markup, and per the house style a
   rule is not deleted the day one page stops using it. The three image
   files it needs — close-white.svg, close-blue.svg and the 2015
   delete-w-www2015.png — go out with it. */
.order-overlay__close {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 2;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 1px solid var(--c-white);
    border-radius: 50%;
    background-color: transparent;
    background-image: url('../img/icons/close-white.svg');
    background-position: center center;
    background-repeat: no-repeat;
    background-size: 1rem;
    opacity: .75;
    cursor: pointer;
    transition: var(--transition);
}

.order-overlay__close:hover,
.order-overlay__close:focus-visible {
    opacity: 1;
}

/* ==========================================================================
   _book3d_overlay — the 3D book, full screen
   Added 2026-09-18. Driven by initFullScreenOverlays(). Same tier as the
   order overlay (230): the two are never open together.

   The book keeps book3d.css's own sizing — 80vh tall at most — so it looks
   the size it does in #book-show. `margin: auto` on the container centres it
   in the flex box and, unlike align-items: center, falls back to the top
   when a short landscape phone cannot fit it, so nothing is cut off above.
   ========================================================================== */
.book3d-overlay {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 230;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-12) 0 var(--space-8);
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream);
}

.book3d-overlay.open {
    display: flex;
}

/* overflow-x: clip for the book's ground shadow, which spreads to 1.28x a
   cover and would give a phone a sideways scroll — the same reason and the
   same fix as .book-viewer. Outside the 3D context, as the house style asks. */
.book3d-overlay > .w-container {
    width: 100%;
    margin: auto;
    overflow-x: clip;
    text-align: center;
}

/* The round outlined close, dark, for a panel on the light ground — the sign-in
   panel (Zlatan, 2026-09-21: "change close button to new design") and the order
   panel built from it. A modifier on .modal-close rather than a new element,
   so the panels' close scripts, which find .modal-close / [data-*-close], are
   untouched; the old grey disc stays for the panels that have not moved over. */
/* SUPERSEDED 2026-09-25 by _close_x, which every live page now uses.
   Kept because the two parked archives (product-original.html,
   product-v01.html) still carry this markup, and per the house style a
   rule is not deleted the day one page stops using it. The three image
   files it needs — close-white.svg, close-blue.svg and the 2015
   delete-w-www2015.png — go out with it. */
.modal-close.close-round {
    top: var(--space-2);
    right: var(--space-2);
    width: 2.25rem;
    height: 2.25rem;
    border: 1px solid var(--c-blue-900);
    background-color: transparent;
    background-image: url('../img/icons/close-blue.svg');
    background-size: 1rem;
    opacity: .75;
    transition: var(--transition);
}

.modal-close.close-round:hover,
.modal-close.close-round:focus-visible {
    opacity: 1;
}

/* The order overlay's round close, drawn dark for the light ground. */
/* SUPERSEDED 2026-09-25 by _close_x, which every live page now uses.
   Kept because the two parked archives (product-original.html,
   product-v01.html) still carry this markup, and per the house style a
   rule is not deleted the day one page stops using it. The three image
   files it needs — close-white.svg, close-blue.svg and the 2015
   delete-w-www2015.png — go out with it. */
.book3d-overlay__close {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 2;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 1px solid var(--c-blue-900);
    border-radius: 50%;
    background-color: transparent;
    background-image: url('../img/icons/close-blue.svg');
    background-position: center center;
    background-repeat: no-repeat;
    background-size: 1rem;
    opacity: .75;
    cursor: pointer;
    transition: var(--transition);
}

.book3d-overlay__close:hover,
.book3d-overlay__close:focus-visible {
    opacity: 1;
}

/* The cover row from the saved-chart confirm, centred over the heading. Its
   text inherits the dark ground's white; the label and price bring their own
   orange. */
.order-overlay .chart-confirm__product {
    justify-content: center;
    max-width: var(--max-form-width);
    margin: 0 auto var(--space-6);
    border-bottom-color: var(--c-white-55);
}

/* ==========================================================================
   _summary_layout — the list, and the summary beside it
   Added 2026-09-21 for cart.html and checkout.html (3.5 and 3.6).

   Two columns from 900px: what is being bought, and what it adds up to. The
   summary is sticky there, so the total and the button stay in view however
   long the list gets — a cart with three books and three messages is taller
   than a laptop screen. One column below 900, the list first: the reader has
   to see what they are paying for before the sum of it means anything.

   text-align: left opts the whole block out of the phone-centring rule at
   the top of this file. It is cards and a form, the two things that rule
   already names as worse centred.
   ========================================================================== */
.summary-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-8);
    align-items: start;
    text-align: left;
}

@media (min-width: 900px) {
    .summary-layout {
        grid-template-columns: minmax(0, 1fr) 20rem;
    }

    .summary-layout > .order-summary {
        position: sticky;
        top: var(--space-4);
    }
}

/* ==========================================================================
   _cart_line — one book, and the birth chart it is made from
   Added 2026-09-21. The page list: "combine product and birth data in one
   card".

   ON A LIGHT CARD (Zlatan, 2026-09-21: "book cards + forms" are the light
   set on these pages). It is the same pairing as order-confirmation.html's
   card — this book, from this chart — seen once more before paying, so it
   keeps that card's ground.

   The book above, the chart below, in one column beside the cover. Not side
   by side like the order page: a cart line sits in a column two-thirds of
   the container wide, and three columns in 580px would leave the chart
   facts ~200px to wrap in. The rule between book and chart is the one the
   order page draws, turned horizontal.

   Prices are --c-blue-900, not the orange .price-text. That orange measures
   1.7:1 on cream; a price there is decoration, and in a cart it is a number
   somebody is checking.
   ========================================================================== */
.cart-lines {
    display: grid;
    gap: var(--space-4);
    margin: 0 0 var(--space-8);
}

.cart-line {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    gap: 0 var(--space-4);
    padding: var(--space-4);
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream);
    color: var(--c-text);

    /* border-radius: var(--radius) */
}

@media (min-width: 600px) {
    .cart-line {
        grid-template-columns: 9rem minmax(0, 1fr);
        gap: 0 var(--space-6);
        padding: var(--space-6);
    }
}

.cart-line__image img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    background-color: var(--c-white);
}

.cart-line__head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-3);
}

.cart-line__name {
    margin: 0;
    font-size: var(--fs-xl);
    font-family: var(--font-heading-fat);
    font-weight: var(--fw-bold);
    line-height: var(--lh-tight);
    text-transform: uppercase;
    color: var(--c-blue-800);
    overflow-wrap: anywhere;
}

.cart-line__name a {
    color: inherit;
}

.cart-line__name a:hover {
    color: var(--c-green);
}

.cart-line__price {
    flex: 0 0 auto;
    margin: 0;
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);

    /* Dark blue on the cream card — see .chart-confirm__product .price-text. */
    color: var(--c-blue-900);
    font-variant-numeric: tabular-nums;
}

.cart-line__chart {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--c-tan);
}

/* The name is the line that must be right — it is printed on the cover. The
   same reasoning as .chart-fact-name, at the size a list line allows. */
.cart-line__person {
    margin: 0;
    font-family: var(--font-heading-fat);
    font-size: var(--fs-lg);
    color: var(--c-blue-800);
    overflow-wrap: anywhere;
}

.cart-line__person .chart-fact-aside {
    color: var(--c-text-soft);
}

.cart-line__facts {
    margin: 0 0 var(--space-2);

    /* font-weight: var(--fw-light) */
    line-height: var(--lh-snug);
}

/* The message and the remove control run the full width of the card, under
   the cover as well: on a phone the cover column is 5.5rem, and a textarea
   squeezed beside it is a textarea nobody wants to write in. */
.cart-line__foot {
    grid-column: 1 / -1;
    margin-top: var(--space-4);
    padding-top: var(--space-3);
    border-top: 1px solid var(--c-tan);
}

.cart-line__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-2) var(--space-4);
}

/* Remove keeps the right-hand end whether or not "Add a message" is beside it. */
.cart-line__actions .text-button--quiet {
    margin-left: auto;

    /* padding-left: 20px */
    /* background-image: url('../img/icons/remove.svg') */
    /* background-repeat: no-repeat */
    /* background-size: 1rem */
    /* background-position: left center */
}

/* A control that reads as a link: the secondary actions on a card, where a
   second button beside "Edit details" would be one too many things shouting.
   A <button>, because it changes the page rather than going anywhere. */
.text-button {
    padding: 0;
    border: 0;
    background: none;
    font-family: var(--font-body);
    font-size: var(--fs-base);
    font-weight: var(--fw-semibold);
    color: var(--c-orange-400);
    cursor: pointer;
    transition: color var(--transition);
}

.text-button:hover,
.text-button:focus-visible {
    color: var(--c-green);
}

/* Remove is quieter than everything else on the card. It is the one action
   nobody came here to do, and Undo makes a slip cheap, so it does not need to
   be hard to reach — only easy to miss when you are not looking for it. */
.text-button--quiet {
    font-weight: var(--fw-regular);
    color: var(--c-text-soft);
}

/* ---- The message for the first page ----
   The live site's "posvetilo": up to 250 characters, printed exactly as typed.
   Per book, not per order (Zlatan, 2026-09-21), so two gifts in one cart can
   carry two messages. data-dedication-state says which of three it is:
   closed (no message, folded), open (the field), filled (a message is
   written: it is shown back, quoted, with "Change"). A line that already has
   a message renders filled; 2026-10-04, it was "open" before. */
.dedication:not([data-dedication-state="open"]) [data-dedication-part="open"],
.dedication:not([data-dedication-state="closed"]) [data-dedication-part="closed"],
.dedication:not([data-dedication-state="filled"]) [data-dedication-part="filled"] {
    display: none;
}

/* The written message, shown back. It is the buyer's own words, so it keeps
   their line breaks, and a long unbroken string cannot widen the card. */
.dedication-saved {
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--c-tan);
}

#confirm-data .dedication-saved {
    padding-top: var(--space-3);
    border-top: 1px solid var(--c-tan);
}

.dedication-saved .w-label {
    margin-bottom: var(--space-2);
    font-weight: var(--fw-semibold);
    color: var(--c-blue-900);
}

.message-quote {
    margin: 0;
    padding-left: var(--space-3);
    border-left: 3px solid var(--c-orange);
    font-style: italic;
    line-height: var(--lh-relaxed);
    white-space: pre-line;
    overflow-wrap: anywhere;
}

.dedication-toggle {
    padding-left: 1.4rem;
    background: url('../img/icons/plus-blue.svg') no-repeat .15rem 48% / .85rem;
}

.dedication-field {
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--c-tan);
}

#confirm-data .dedication-field {
    margin-bottom: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--c-tan);
}

.dedication-field .w-label {
    margin-bottom: var(--space-2);
    font-weight: var(--fw-semibold);
    color: var(--c-blue-900);
}

/* The label and its character count on one line, the count at the right. */
.label-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
}

/* In the label row the counter is always on, so it has no .show to wait for. */
.label-row .char-counter {
    display: block;
    margin-left: 0;
}

/* A message is read in a book, not typed in a line, so it is set in a
   textarea at the body size and grows with the reader's words. */
.dedication-field textarea.w-input {
    min-height: 6.5rem;
    font-size: var(--fs-base);
    font-weight: var(--fw-regular);
    line-height: var(--lh-relaxed);
    resize: vertical;
}

.field-hint {
    margin: var(--space-2) 0 0;
    font-size: var(--fs-sm);
    font-weight: var(--fw-light);
    line-height: var(--lh-snug);
    color: var(--c-blue-950-80);
}

.checkout-card .email-capture .field-hint {
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
}

/* ---- Removed, with Undo ----
   No "are you sure?". The line folds to one sentence on the dark ground — it
   is no longer a book, so it loses the light card — and Undo puts it back.
   data-line-state; in Rails "Remove" is a request and Undo re-adds the same
   book, chart and message. */
.cart-line[data-line-state="removed"] {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--space-3) var(--space-4);
    border: 1px dashed var(--c-white-55);
    background: none;
    color: var(--c-white-75);
}

/* "listed", not "in": a bare ?state=in already means signed in, and a value
   two states share stops working as a bare preview word. */
.cart-line[data-line-state="removed"] > :not(.cart-line__removed),
.cart-line[data-line-state="listed"] > .cart-line__removed {
    display: none;
}

.cart-line__removed {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-2) var(--space-4);
}

.cart-line__removed p {
    margin: 0;
}

/* ---- Another book, and the three promises ---- */
.cart-more {
    margin-bottom: var(--space-8);
    padding: var(--space-4) 0;
    border-top: 1px solid var(--c-white-25);
    border-bottom: 1px solid var(--c-white-25);
    text-align: center;
}

.cart-more p {
    margin: 0;
}

.cart-promise {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-4);
}

@media (max-width:599px) {
    .cart-promise {
        display: inline-grid;

        /* margin: 0 auto */
    }
}

@media (min-width: 600px) {
    .cart-promise {
        grid-template-columns: repeat(3, 1fr);
    }
}

.cart-promise li {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    color: var(--c-white-75);
}

/* From 900px the three promises stand as a row of three, icon above the
   words and centred (Zlatan, 2026-09-24). Below that they stay a list,
   where an icon beside the line reads faster in one narrow column. */
@media (min-width: 900px) {
    .cart-promise li {
        flex-direction: column;
        align-items: center;
        text-align: center;
        gap: var(--space-2);
    }
}

/* The sentence is its own <p>, not loose text beside the icon, so it is one
   clean unit for the text file and the icon's markup stays out of it. */
.cart-promise p {
    margin: 0;
}

/* .feature-icon at a list size. The icons are the ones the feature grids use,
   so the promise looks like the same promise it is on home and product. */
.cart-promise .feature-icon {
    flex: 0 0 auto;
    width: var(--space-12);
    height: var(--space-12);
    margin: 0;
    background-size: 2rem;
}

/* ---- The empty cart ----
   data-cart-state on <article>, so the hero and the body change together. */
[data-cart-state="full"] [data-cart-part="empty"],
[data-cart-state="empty"] [data-cart-part="full"] {
    display: none;
}

.cart-empty {
    max-width: var(--max-form-width);
    margin: 0 auto;
    text-align: center;
}

/* With the summary gone, the list takes the whole row rather than leaving a
   20rem hole where the summary stood. */
[data-cart-state="empty"] .summary-layout {
    grid-template-columns: minmax(0, 1fr);
}

/* An empty cart in Rails renders no lines at all. On this page ?state=empty
   leaves the sample lines in the markup, so the listed ones step aside; a
   removed one keeps its Undo, which is how the page got empty. */
[data-cart-state="empty"] .cart-line[data-line-state="listed"] {
    display: none;
}

/* .cart-badge is display: flex, which outranks the browser's own [hidden]. */
.cart-badge[hidden] {
    display: none;
}

/* ==========================================================================
   _order_summary — what it all adds up to
   Added 2026-09-21. Dark (Zlatan: "summary stays dark"), with the 7px
   --c-blue-700 top seam the bands already carry (house style: seams are
   drawn). The total is the one orange number on the page.
   ========================================================================== */
.order-summary {
    padding: var(--space-6);
    border-top: 7px solid var(--c-blue-700);
    background-color: var(--c-blue-950);
    box-shadow: var(--shadow-tight);
    color: var(--c-white-85);
}

.order-summary__head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.order-summary__title {
    margin: 0;
    font-family: var(--font-heading-fat);
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    color: var(--c-white-85);
}

.summary-rows {
    margin: 0 0 var(--space-1);
}

.summary-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-4);
    padding: var(--space-2) 0;
    border-bottom: 1px dotted var(--c-white-25);
}

.summary-row dt {
    font-weight: var(--fw-light);
    color: var(--c-white-75);
}

.summary-row dd {
    margin: 0;
    font-weight: var(--fw-semibold);
    color: var(--c-white-soft);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.summary-row--total {
    padding-top: var(--space-3);
    border-bottom: 0;
}

.summary-row--total dt {
    font-family: var(--font-heading);
    font-size: var(--fs-lg);
    color: var(--c-white-soft);
}

.summary-row--total dd {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    color: var(--c-orange);
}

/* In the EU the price a consumer sees must include VAT, so it comes out of
   €76, not on top (2026-09-13). Outside the EU Rails leaves this line out —
   the Article 52 export exemption — rather than saying anything else. */
.summary-vat {
    margin: 0 0 var(--space-4);
    font-size: var(--fs-xs);
    color: var(--c-white-55);
    text-align: right;
}

.summary-button {
    display: block;
    width: 100%;
}

.summary-note {
    margin: var(--space-4) 0 0;
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    color: var(--c-white-75);
}

.summary-edit {
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
}

/* The same panel standing on its own, centred, where there is no form beside
   it — the bank transfer page. Added 2026-09-24. */
.order-summary--standalone {
    max-width: var(--max-form-width);
    margin: 0 auto;
}

/* "Saved", beside the message's label. It appears when the field is left and
   goes again; the word is in the markup, and only shown from the script. */
.field-saved {
    margin-left: auto;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    color: var(--c-green);
}

/* ---- The books, small, at the top of the checkout summary ---- */
.summary-lines {
    margin: 0 0 var(--space-3);
}

.summary-line {
    display: grid;
    grid-template-columns: 3.5rem minmax(0, 1fr) auto;
    gap: var(--space-3);
    align-items: center;
    padding: var(--space-3) 0;
    border-bottom: 1px dotted var(--c-white-25);
}

.summary-line img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

.summary-line p {
    margin: 0;
    line-height: var(--lh-tight);
    overflow-wrap: anywhere;
}

.summary-line__name {
    font-weight: var(--fw-semibold);
    color: var(--c-white-soft);
}

.summary-line__for {
    font-size: var(--fs-sm);
    color: var(--c-white-75);
}

.summary-line__price {
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
}

/* ---- Gift card or discount code ----
   Folded behind one line: most carts have no code, and an empty field in the
   summary invites a search for one. data-code-state="none|applied"; which
   half shows is CSS, and the field itself opens with its own toggle. The
   gift card is the pre-paid book of Bible §10, so it lives here too. */
.code-box {
    margin-bottom: var(--space-4);
}

/* The state sits on the whole summary, not on the code box, because the
   discount row in the sums above belongs to the same code. */
[data-code-state="none"] [data-code-part="applied"],
[data-code-state="applied"] [data-code-part="none"] {
    display: none;
}

.code-toggle {
    font-size: var(--fs-sm);
}

.code-form .form-row {
    margin-bottom: 0;
    padding-bottom: 0;
}

.code-row {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

.code-row .w-input {
    min-height: 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-base);
    text-transform: uppercase;
}

.code-row .w-input::placeholder {
    text-transform: none;
}

.code-row .button {
    flex: 0 0 auto;
    padding: var(--space-2) var(--space-4);
    font-size: var(--fs-sm);
}

.code-box .error-message {
    color: var(--c-orange);
}

.code-applied {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-white-75);
}

.code-applied b {
    color: var(--c-white-soft);
    letter-spacing: 1px;
}

/* ==========================================================================
   _checkout_bar — the total and the button, on phones
   Added 2026-09-21. Below 900px the summary sits under every book, so on a
   phone with two books and a message it is two screens down. This bar keeps
   the total and the way on in reach, and steps aside whenever the summary's
   own button is on screen — two identical buttons in view would read as two
   different things. .visible is set by initCheckoutBar(); without a script
   the bar never shows and the summary's button is still there.
   ========================================================================== */
.checkout-bar {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 200;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--c-blue-700);
    background-color: var(--c-blue-950-80);
    box-shadow: var(--shadow-tight);
    backdrop-filter: blur(6px);
}

@media (max-width: 899px) {
    .checkout-bar.visible {
        display: flex;
    }
}

.checkout-bar p {
    margin: 0;
    line-height: var(--lh-tight);
    color: var(--c-white-75);
}

.checkout-bar b {
    display: block;
    font-size: var(--fs-xl);
    color: var(--c-orange);
    font-variant-numeric: tabular-nums;
}

.checkout-bar .button {
    padding: var(--space-3) var(--space-4);
    font-size: var(--fs-base);
}

/* ==========================================================================
   _checkout_steps — delivery, then payment, on one light card
   Added 2026-09-21 (3.6). The page list's "two tabs: address + payment".

   Steps, not tabs: payment cannot come before an address, and a tab says you
   may pick either. One page, no reload, the same shape as the free chart
   confirm (house style: a step within a page is a state attribute).
   data-checkout-state="delivery|payment". A finished step folds to a recap
   with "Change", which is the only way back — and the reader can always see
   what the parcel will say on it while they pay.

   ON A LIGHT CARD: it is a form, like the birth chart form (Zlatan,
   2026-09-21). The card is the chart form's own light gradient.
   ========================================================================== */
/* Ground from .bgr-cream in the markup — see .chart-form-wrapper. */
.checkout-card {
    padding: var(--space-6) var(--space-4) var(--space-8);
    color: var(--c-text);
}

@media (min-width: 600px) {
    .checkout-card {
        padding: var(--space-8);
    }
}

.checkout-step + .checkout-step {
    margin-top: var(--space-6);
    padding-top: var(--space-6);
    border-top: 1px solid var(--c-tan);
}

.step-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

/* The number is a plain circle, not an icon: the two steps are an order, and a
   number is the one mark that says "this, then that" in any language. */
.step-number {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    background-color: var(--c-blue-900);
    color: var(--c-white-soft);
    font-weight: var(--fw-bold);
    line-height: 1;
}

.step-title {
    flex: 1 1 auto;
    margin: 0;
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    color: var(--c-blue-900);
}

/* initCheckout moves focus to the new step's title, so a screen reader
   announces where the reader now is. It is a heading, not a control, and
   the ring round it only looked like something to press. */
.step-title:focus {
    outline: none;
}

/* A step the reader has not reached is present and quiet — they can see that
   payment comes next, and nothing in it can be touched yet. */
[data-checkout-state="delivery"] [data-checkout-part="payment"] .step-number {
    background-color: var(--c-tan);
}

[data-checkout-state="delivery"] [data-checkout-part="payment"] .step-title {
    color: var(--c-tan);
}

[data-checkout-state="delivery"] [data-checkout-part="payment"] .step-body,
[data-checkout-state="payment"] [data-checkout-part="delivery"] .step-body,
[data-checkout-state="delivery"] .step-recap,
[data-checkout-state="delivery"] .step-change {
    display: none;
}

.step-recap {
    margin: 0 0 0 calc(2rem + var(--space-3));
    line-height: var(--lh-snug);

    /* An email address has no spaces to break at, and a long one ran past
       the card at 375px. */
    overflow-wrap: anywhere;
}

.step-recap p {
    margin: 0 0 var(--space-1);
}

/* One address line per span. Direct children only: postcode and town are two
   spans inside one line and must stay on it. */
.step-recap p > span {
    display: block;
}

.step-recap span:empty {
    display: none;
}

.step-change {
    flex: 0 0 auto;
}

/* The next step unveils top to bottom, the same move as the free chart
   confirm's email question, so the two checkouts in this site feel like one. */
[data-checkout-state="payment"] [data-checkout-part="payment"] .step-body {
    animation: step-unveil .7s ease-out backwards;
}

@media (prefers-reduced-motion: reduce) {
    [data-checkout-state="payment"] [data-checkout-part="payment"] .step-body {
        animation: none;
    }
}

/* ---- Fields ----
   The birth form's rows and inputs, with the long form's dotted divider kept:
   this is a long form too. Labels are visible here, unlike the confirm card's
   single field — an address is six questions, not one. */
.checkout-form .w-label {
    margin-bottom: var(--space-2);
    font-weight: var(--fw-regular);
}

.checkout-form .form-row {
    margin-bottom: var(--space-4);
}

.checkout-form .w-select {
    width: 100%;
    margin-right: 0;
    background-position: right var(--space-4) center;
}

.label-aside {
    font-weight: var(--fw-light);
    color: var(--c-text-soft);
}

/* A row that is a link until it is wanted — the second address line. The
   divider goes with it: a rule under one short link reads as a heading for
   the row below. */
.form-row--quiet {
    border-bottom: 0;
    padding-bottom: 0;
}

[data-extra-state="closed"] [data-extra-part="open"],
[data-extra-state="open"] [data-extra-part="closed"] {
    display: none;
}

/* The arrow points at the step below, so it is an icon on the left like every
   other button's (Zlatan, 2026-09-24 — the character version was the odd one
   out). --c-white-soft is not a token on the icon, so the white file is used
   as it is, at the size the calculate button uses. */
.button--down {
    padding-left: 3.25rem;
    background-image: url('../img/icons/chevron-down-white.svg');
    background-repeat: no-repeat;
    background-position: var(--space-6) 50%;
    background-size: .9rem;
}

/* Postcode and town share a row from 600px, postcode the narrower. On a phone
   they stack: two inputs side by side at 375px are two inputs too small to
   read what was typed into them. */
.form-pair {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0 var(--space-4);
}

@media (min-width: 600px) {
    .form-pair {
        grid-template-columns: 2fr 3fr;
    }
}

.checkout-signin {
    margin: 0;
    font-size: var(--fs-sm);
}

/* ---- Email, known or asked for ----
   .email-capture's two halves (data-email-state="new|known") without its
   centred confirm-card framing: here it is the first row of a form. */
/* `margin: 0` here used to cancel .form-row's own bottom margin, so the
   "Full name" label sat flush against the dotted line above it — no air at
   all (Zlatan spotted it, 2026-09-24). The email is also the end of a group,
   so it takes the wider gap rather than the standard row one. */
.checkout-form .email-capture {
    max-width: none;
    margin: 0 0 var(--space-6);
    padding-top: 0;
    border-top: 0;
    text-align: left;
}

.checkout-form .email-address {
    margin: 0;
}

/* ---- Saved addresses, for a signed-in customer ----
   data-address-state="list|form". The list is radio cards; "A different
   address" swaps it for the empty form. Page list: "we might have user
   addresses in database". */
[data-address-state="list"] [data-address-part="form"],
[data-address-state="form"] [data-address-part="list"] {
    display: none;
}

.choice-list {
    display: grid;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

/* A radio whose whole card is the target. The native input stays in the
   card, visible and focusable: it is what a keyboard and a screen reader
   actually operate, and restyling it away would mean rebuilding both. */
.choice-card {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--c-border);
    background-color: var(--c-white);
    cursor: pointer;
    transition: border-color var(--transition), background-color var(--transition);
}

/* Two shapes. A saved address is all words, so the whole card is its <label>.
   A payment choice holds Stripe's fields, and a label cannot wrap a form area,
   so there the card is a <div> and its title is the label. */
div.choice-card {
    cursor: default;
}

.choice-card label {
    display: block;
    cursor: pointer;
}

.choice-card:hover {
    border-color: var(--c-orange);
}

.choice-card:has(input:checked) {
    border-color: var(--c-green);
    box-shadow: inset 0 0 0 1px var(--c-green);
}

.choice-card input[type="radio"] {
    flex: 0 0 auto;
    width: 1.15rem;
    height: 1.15rem;
    margin: .2rem 0 0;
    accent-color: var(--c-green);
}

.choice-card__body {
    flex: 1 1 auto;
    min-width: 0;
    line-height: var(--lh-snug);
}

.choice-card__title {
    display: block;
    font-weight: var(--fw-semibold);
    color: var(--c-blue-900);
}

.choice-card__text {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--fs-sm);
    font-weight: var(--fw-light);
}

/* ---- Payment methods ----
   Two choices. The card one holds the space Stripe's Payment Element fills in
   Phase 4 (card, Apple Pay, Google Pay — whichever the device offers); the
   bank transfer is ours. The panel under a choice shows only while it is
   chosen, so the step stays one screen tall. */
.choice-card__panel {
    display: none;
    margin-top: var(--space-3);
}

.choice-card:has(input:checked) .choice-card__panel {
    display: block;
}

/* The footer's text badges, drawn dark for the light card. */
.payment-methods--light {
    margin: var(--space-2) 0 0;
}

.payment-methods--light li {
    border-color: var(--c-border);
    color: var(--c-text-soft);
}

/* PLACEHOLDER for Stripe. Three still fields in a dashed frame, so the
   height of the step is honest while nobody mistakes it for the real thing. */
.stripe-slot {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px dashed var(--c-border);
    background-color: var(--c-white-75);
}

.stripe-slot .w-input {
    min-height: 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-base);
    pointer-events: none;
}

.stripe-slot .w-input:first-child {
    grid-column: 1 / -1;
}

/* Bank transfer is offered in Slovenia only (the footer has said so since
   2026-09-16). Rails renders the option or leaves it out per shipping
   country; on this page the country is chosen a step earlier, so the script
   hides it too — see data-countries. */
.choice-card[hidden] {
    display: none;
}

.checkout-terms {
    margin: var(--space-4) 0;
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    color: var(--c-text-soft);
}

.checkout-actions {
    text-align: center;
}

/* ---- The two reassurances, at the foot of the light card ----
   Added 2026-09-24 (Zlatan). Quiet: they are there for the reader who looks
   for them, and they must not compete with the pay button above. The icons are
   the footer's lock and the feature grid's shield, drawn small and dark for
   the cream ground. */
.checkout-proofs {
    margin: var(--space-6) 0 0;
    padding-top: var(--space-4);
    border-top: 1px solid var(--c-tan);
    display: grid;
    gap: var(--space-3);
}

.checkout-proof {
    padding-left: var(--space-8);
    background-repeat: no-repeat;
    background-position: 0 .1em;
    background-size: 1.1rem;
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    color: var(--c-text-soft);
}

.checkout-proof--lock {
    background-image: url('../img/icons/lock-blue.svg');
}

.checkout-proof--shield {
    background-image: url('../img/icons/shield-blue.svg');
}

.checkout-actions .button {
    width: 100%;
    max-width: 24rem;
}

/* data-pay-variant="due|covered". A gift card that covers the whole order
   leaves nothing to pay: the methods go, and the button says so. */
[data-pay-variant="due"] [data-pay-part="covered"],
[data-pay-variant="covered"] [data-pay-part="due"] {
    display: none;
}

/* Covered has no payment step (2026-10-04): the delivery step is the whole
   form and ends in "Place the order". Its number would be "1" of one. */
[data-pay-variant="covered"] [data-checkout-part="payment"],
[data-pay-variant="covered"] .step-number {
    display: none;
}

/* data-delivery-variant="post|download". An order of ebooks only has no
   parcel, so step 1 asks for the email alone. */
[data-delivery-variant="download"] [data-delivery-part="post"],
[data-delivery-variant="post"] [data-delivery-part="download"] {
    display: none;
}

/* ---- The checkout summary folds away on phones ----
   Above 900 it is always open beside the form. Below, it sits ABOVE the form
   as one line — "Your order · €110" — because the form is the job and the
   list is a reference. data-summary-state="closed|open". */
.summary-fold {
    display: none;
}

@media (max-width: 899px) {
    .summary-fold {
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
        padding: 0;
        border: 0;
        background: none;
        font-family: var(--font-body);
        font-size: var(--fs-base);
        color: var(--c-white-85);
        cursor: pointer;
    }

    .summary-fold b {
        font-size: var(--fs-xl);
        color: var(--c-orange);
    }

    .summary-fold span::after {
        content: "";
        display: inline-block;
        width: .75rem;
        height: .75rem;
        margin-left: var(--space-2);
        background: url('../img/icons/chevron-down-white.svg') no-repeat center / contain;
        transition: transform var(--transition);
    }

    [data-summary-state="open"] .summary-fold span::after {
        transform: rotate(180deg);
    }

    [data-summary-state="closed"] .summary-fold-body {
        display: none;
    }

    [data-summary-state="open"] .summary-fold-body {
        margin-top: var(--space-4);
    }

    .checkout-layout .order-summary {
        order: -1;
        padding: var(--space-4);
    }

    /* The fold already says "Your order"; "Edit cart" stays. */
    .checkout-layout .order-summary__title {
        display: none;
    }

    .checkout-layout .order-summary__head {
        justify-content: flex-end;
    }
}

/* ==========================================================================
   _next_steps — what happens after paying
   Added 2026-09-21 for the success and bank transfer pages (3.7, 3.8).
   Three steps across from 768px, a column below. Dark: the end pages stay
   dark (Zlatan, 2026-09-21).
   ========================================================================== */
/* An <ol>, because the steps are an order; the base reset covers <ul> only,
   so the list style is cleared here and the number is drawn by the counter. */
.next-steps {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-6);

    /* margin: 0 0 var(--space-8) */
    padding: 0;
    list-style: none;
    counter-reset: next-step;
}

@media (min-width: 768px) {
    .next-steps {
        grid-template-columns: repeat(3, 1fr);
    }
}

.next-step {
    position: relative;
    padding-top: var(--space-4);
    border-top: 2px solid var(--c-blue-700);
    text-align: left;
}

/* The step that is happening now carries the orange rule: the reader is
   somewhere on this line, and this is where. */
.next-step--now {
    border-top-color: var(--c-orange);
}

.next-step__title {
    margin: 0 0 var(--space-2);
    font-size: var(--fs-lg);
    font-weight: var(--fw-bold);
    color: var(--c-white-85);
}

.next-step__title::before {
    counter-increment: next-step;
    content: counter(next-step) ". ";
    color: var(--c-orange);
}

.next-step p {
    margin: 0;
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    color: var(--c-white-75);
}

/* ==========================================================================
   _order_recap — the order as it was placed
   Added 2026-09-21 for the three end pages. A receipt, on the dark ground:
   each book with the chart it is made from, then where it goes and how it
   was paid. The covers stay photographs, the one light thing in it.
   ========================================================================== */
.order-recap {
    max-width: 44rem;
    margin: 0 auto;
    text-align: left;
    border-top: 1px solid var(--c-tan);
}

.recap-line {
    display: grid;
    grid-template-columns: 4.5rem minmax(0, 1fr) auto;
    gap: var(--space-4);
    align-items: start;
    padding: var(--space-4) 0;
    border-bottom: 1px solid var(--c-white-25);
}

@media (min-width: 600px) {
    .recap-line {
        grid-template-columns: 6rem minmax(0, 1fr) auto;
    }
}

.recap-line img {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

.recap-line p {
    margin: 0 0 var(--space-1);
    line-height: var(--lh-snug);
    overflow-wrap: anywhere;
}

.recap-line__name {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-lg);
    color: var(--c-white-soft);
}

/* Who it is for, and the birth details under it: two lines, because the first
   is copy with a name in it and the second is the chart's own data. */
.recap-line__chart,
.recap-line__facts {
    font-size: var(--fs-sm);
    color: var(--c-white-75);
}

.recap-line__chart {
    margin-bottom: 0 !important;
}

/* The message is shown as it will be printed — the customer's own words, in
   their own line breaks, set apart so a typo is visible now rather than on
   the first page of a gift. */
.recap-line__message {
    margin-top: var(--space-2) !important;
    padding-left: var(--space-3);
    border-left: 2px solid var(--c-orange-50);
    font-style: italic;
    font-size: var(--fs-sm);
    color: var(--c-white-75);
    white-space: pre-line;
}

.recap-line__price {
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
}

.recap-line .button {
    margin-top: var(--space-2);
    padding: var(--space-2) var(--space-4);
    font-size: var(--fs-sm);
}

.recap-facts {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-6);
    margin: var(--space-6) 0 0;
}

@media (min-width: 600px) {
    .recap-facts {
        /* grid-template-columns: 1fr 1fr */
    }
}

.recap-facts dt {
    margin-bottom: var(--space-1);
}

.recap-facts dd {
    margin: 0;
    line-height: var(--lh-snug);
    color: var(--c-white-85);
}

.recap-facts dd span {
    display: block;
}

.recap-total {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-top: var(--space-6);
    padding-top: var(--space-4);
    border-top: 1px solid var(--c-white-25);
}

.recap-total b {
    font-size: var(--fs-2xl);
    color: var(--c-orange);
    font-variant-numeric: tabular-nums;
}

.recap-ask {
    margin: var(--space-12) 0 0;
    text-align: center;
}

/* ---- The order on the light band (thank-you page, 2026-09-24) ----
   The recap was built for a dark ground and is reused here on cream, so every
   colour it states has to be restated. Listed in one block rather than spread
   through the rules above, so the pair is obvious: change one, look here.
   The total follows the light-ground price rule — dark blue, not orange. */
.order-recap-section {
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream);
    color: var(--c-text);
}

.dark-heading {
    color: var(--c-blue-900);
    font-family: var(--font-heading);
    font-size: var(--fs-4xxl);
    line-height: var(--lh-tight);
    letter-spacing: 0;
    font-weight: var(--fw-semibold);
}

.order-recap-section .recap-line,
.order-recap-section .recap-total {
    border-color: var(--c-tan);
}

.order-recap-section .recap-line__name,
.order-recap-section .recap-facts dd,
.order-recap-section .recap-total b {
    color: var(--c-blue-900);
}

.order-recap-section .recap-line__chart,
.order-recap-section .recap-line__facts,
.order-recap-section .recap-line__message {
    color: var(--c-text-soft);
}

.recap-line p.gift-code-order {
    border: 1px solid var(--c-text-soft) ;
    padding-left: .5rem;
    padding-right: .5rem;
    margin-top: .25rem;
    font-size: var(--fs-lg);
    display: inline-block;
    line-height: --lh-loose !important;
    background-color: var(--c-white);
}

.recap-line p.gift-code-order span {
    font-weight: 800;
    color: var(--c-blue-900);
}

/* The code copies itself when pressed (2026-10-04). The icon says it is
   pressable on a phone, where there is no hover to show the bubble; the
   bubble is the .info-bubble, shown on hover and focus, and held open for
   two seconds after a press with its message changed (.is-copied). */
.recap-line p.gift-code-order[data-copy-text] {
    position: relative;
    padding-right: 1.9rem;
    background: var(--c-white) url('../img/icons/copy-blue.svg') no-repeat right .45rem center / 1rem;
    cursor: pointer;
}

.gift-code-order[data-copy-text] .info-bubble {
    font-weight: var(--fw-regular);
    font-size: var(--fs-xs);
    letter-spacing: 0;
    color: var(--c-white);
}

.gift-code-order[data-copy-text]:focus-visible {
    outline: 2px solid var(--c-orange);
    outline-offset: 2px;
}

@media (hover: hover) {
    .gift-code-order[data-copy-text]:hover .info-bubble {
        opacity: 1;
        transform: translate(-50%, 0);
    }
}

.gift-code-order[data-copy-text]:focus-visible .info-bubble,
.gift-code-order[data-copy-text].is-copied .info-bubble {
    opacity: 1;
    transform: translate(-50%, 0);
}

/* ==========================================================================
   _cta_block — one offer, on its own
   Added 2026-09-24 (Zlatan: "design it as a CTA block"). The next gift, at
   the foot of the thank-you page. Same panel as the summary and the account
   offer — the 7px seam and the deep ground — so the page ends on a shape it
   has already used twice.
   ========================================================================== */
.cta-block {
    max-width: var(--max-form-width);
    margin: var(--space-8) auto 0;
    padding: var(--space-6);
    border-top: 7px solid var(--c-blue-700);
    background-color: var(--c-blue-900);
    text-align: left;
}

.cta-block p {
    color: var(--c-white-75);
}

/* ==========================================================================
   _end_panel — the payment-error content, held like the bank slip
   Added 2026-09-24 (Zlatan). The error page was loose text on the section
   ground; the bank slip's panel gives it the same edges, so the two pages
   at the end of the checkout look like one pair.
   ========================================================================== */
.end-panel {
    max-width: 44rem;
    margin: 0 auto;
    padding: var(--space-6) var(--space-4);
    border-top: 7px solid var(--c-blue-800);
    background-color: var(--c-blue-900);
    box-shadow: var(--shadow-tight);

    /* Centred throughout (Zlatan, 2026-09-24). This is the one place the
       phone-centring rule's "bullets stay put while the text slides away" is
       answered rather than opted out of: the star moves above each cause, so
       it stays with its line. */
    text-align: center;
}

@media (min-width: 768px) {
    .end-panel {
        padding: var(--space-8);
    }
}

/* ==========================================================================
   _account_offer — follow the order in My astro
   Every order belongs to a user record (Order belongs_to :user), so a buyer
   who never signed in already has an account made from the email. What is
   missing is a password. One field, here, where the email is already known;
   data-account-state="out|in", the header's own attribute.
   ========================================================================== */
.account-offer {
    max-width: var(--max-form-width);
    margin: 0 auto;
    padding: var(--space-6);
    border-top: 7px solid var(--c-blue-700);
    background-color: var(--c-blue-900);
    text-align: left;
}

/* Not scoped to .account-offer since 2026-09-24: the chart page offers an
   account too, under the same attribute, so the pair is stated once. */
[data-account-state="out"] [data-account-part="in"],
[data-account-state="in"] [data-account-part="out"] {
    display: none;
}

.account-offer .form-row {
    border-bottom: 0;
    padding-bottom: 0;
}

.account-offer .w-label {
    margin-bottom: var(--space-2);
}

/* ==========================================================================
   _bank_slip — the bank transfer details (3.8)
   Added 2026-09-21. Everything a banking app asks for, in the order it asks,
   each with its own Copy: a mistyped reference is an order nobody can match
   to its payment. The reference is the orange line, because it is the one
   people leave out. Dark, like the other end pages, with the summary's seam.

   BIC is left out on purpose. A SEPA transfer inside the EU needs the IBAN
   alone, and the BIC in sl.yml (HDELSI22) does not belong to the bank the
   IBAN's code names — see docs/05-decisions.md, 2026-09-21.
   ========================================================================== */
.bank-slip {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-6);

    /* max-width: 44rem */
    /* margin: 0 auto var(--space-8) */
    padding: var(--space-6) var(--space-4);
    border-top: 7px solid var(--c-blue-700);
    background-color: var(--c-blue-900);
    box-shadow: var(--shadow-tight);
    text-align: left;
}

@media (min-width: 768px) {
    .bank-slip {
        grid-template-columns: minmax(0, 1fr) 10rem;
        padding: var(--space-8);
    }
}

.slip-rows {
    margin: 0;
}

.slip-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0 var(--space-3);
    align-items: center;
    padding: var(--space-3) 0;
    border-bottom: 1px dotted var(--c-white-25);
}

.slip-row dt {
    grid-column: 1 / -1;
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--c-white-55);
}

.slip-row dd {
    margin: 0;
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--c-white-soft);
    overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums;
}

.slip-row--key dt,
.slip-row--key dd {
    color: var(--c-orange);
}

.slip-row__note {
    grid-column: 1 / -1;
    margin: var(--space-1) 0 0;
    font-size: var(--fs-sm);
    color: var(--c-white-75);
}

.copy-button {
    padding: var(--space-1) var(--space-3);
    border: 1px solid var(--c-white-55);
    background: none;
    font-family: var(--font-body);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--c-white-85);
    cursor: pointer;
    transition: var(--transition);
}

.copy-button:hover,
.copy-button:focus-visible {
    border-color: var(--c-orange);
    color: var(--c-orange);
}

.copy-button.is-copied {
    border-color: var(--c-green);
    background-color: var(--c-green);
    color: var(--c-white);
}

/* The QR code a banking app scans. Hidden on phones: this page IS on the
   phone, and a phone cannot photograph its own screen — there, the Copy
   buttons are the shortcut. */
.slip-qr {
    display: none;
}

@media (min-width: 768px) {
    .slip-qr {
        display: block;
        text-align: center;
    }
}

.slip-qr__code {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 10rem;
    aspect-ratio: 1 / 1;
    margin-bottom: var(--space-2);
    border: 1px dashed var(--c-text-soft);
    background-color: var(--c-white);
    color: var(--c-text-soft);
    font-size: var(--fs-xs);
}

.slip-qr p {
    margin: 0;
    font-size: var(--fs-xs);
    line-height: var(--lh-snug);
    color: var(--c-white-75);
}

.slip-foot {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3) var(--space-6);
}

.slip-foot p {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--c-white-75);
}

/* ==========================================================================
   _pay_error — why a payment fails, and what to do (3.9)
   Added 2026-09-21. The reassurance is in the hero; this is the reasons,
   then the ways on. The bank's own reason, when Stripe passes one back, sits
   first: a specific answer beats four possible ones.
   ========================================================================== */
/* The bank's reason, inside the error page's .end-panel: a cream plate with
   an orange rule on the left, the text centred with the rest of the panel
   (Zlatan's pass, 2026-10-14). The label is orange, bold and short; the
   reason itself is --c-text, because orange on cream measures 2:1 and the
   sentence must be read (Zlatan chose this, 2026-10-14). Merged from two
   rules: the reason only ever sits inside the panel. */
.error-reason {
    max-width: 36rem;
    margin: 0 auto var(--space-8);
    padding: var(--space-4) var(--space-6);
    border-left: 7px solid var(--c-orange-400);
    background-color: var(--c-cream);
    color: var(--c-text);
    text-align: center;
}

.error-reason p {
    margin: 0;
}

.error-reason__label {
    font-weight: var(--fw-bold);
    color: var(--c-orange-400);
}

.cause-list {
    display: grid;
    gap: var(--space-3);

    /* max-width: 36rem */
    margin: 0 auto var(--space-8);
    padding-bottom: var(--space-8);
    border-bottom: 1px solid var(--c-white-25);

    /* text-align: left */
}

/* The star sits above the line, not beside it, so a centred cause keeps its
   bullet. Left-hanging bullets and centred text come apart. */
.cause-list li {
    padding-top: var(--space-6);
    background: url('../img/icons/astro-sun-bullet.svg') no-repeat center top / 1rem;
    line-height: var(--lh-snug);
}

.end-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-4);
    margin-bottom: var(--space-8);
    padding-bottom: var(--space-12);
    border-bottom: 1px solid var(--c-white-25);
}

/* ==========================================================================
   _close_x — one close control, drawn in CSS
   Added 2026-09-25, from the pen Zlatan found (MarvinVK, "Pure css close
   button animation"). The technique, not the code: two bars on the
   pseudo-elements, crossed. Written here so it is ours, follows our tokens,
   and can be changed in one place.

   WHY IT IS WORTH REPLACING WHAT WE HAVE. Today there are five close
   controls — .modal-close (a grey disc with a 2015 PNG in it),
   .modal-close.close-round, .gallery-viewer__close, .book3d-overlay__close
   and .close-button — and three image files between them. Drawn in CSS it is
   one class, no requests, and the colour comes from currentColor, so the same
   control works on the cream panels and on the starfield without a second
   file.

   THE X STAYS SMALL, THE BUTTON DOES NOT. The visible cross is 1rem and the
   bars are 1.5px, which is what "small and not so thick" means — but the
   button around it is 2.75rem, because 83% of this site's traffic is a
   thumb. Shrinking the target with the mark is the one way this goes wrong.
   ========================================================================== */
.close-x {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto;
    width: 2.5rem;
    height: 2.5rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: currentcolor;
    cursor: pointer;
    --close-mark: 1.25rem;
    --close-weight: 1.5px;
}

.close-x::before,
.close-x::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--close-mark);
    height: var(--close-weight);
    background-color: currentcolor;
    transition: transform var(--transition);
}

.close-x::before {
    transform: translate(-50%, -50%) rotate(45deg);
}

.close-x::after {
    transform: translate(-50%, -50%) rotate(-45deg);
}

/* Hover only — NOT :focus-visible (2026-09-25). Tying the colour to focus
   made the close look hovered the moment an overlay opened and put focus
   anywhere near it. Keyboard focus is shown by the outline below, which is
   what an outline is for. */
.close-x:hover {
    color: var(--c-orange);
}

.close-x:hover::before {
    transform: translate(-50%, -50%) rotate(135deg);
}

.close-x:hover::after {
    transform: translate(-50%, -50%) rotate(45deg);
}

/* Hover B — the pen's own move: the two bars lie down into one. It is the
   prettier animation and the weaker sign: a single bar is what a MINIMISE
   control looks like, and this closes things. Kept here to be compared, and
   used only if Zlatan prefers it. */
.close-x--fold:hover::before,
.close-x--fold:hover::after {
    transform: translate(-50%, -50%) rotate(0deg);
}

/* A ring, for a control that sits on a photograph or a busy ground and needs
   its own edge — what .close-round does today. */
.close-x--ring {
    border: 1px solid currentcolor;
    opacity: .75;
}

.close-x--ring:hover,
.close-x--ring:focus-visible {
    opacity: 1;
}

.close-x--small {
    width: 2rem;
    height: 2rem;
    --close-mark: .75rem;
    --close-weight: 1.25px;
}

.close-x--large {
    --close-mark: 1.25rem;
    --close-weight: 2px;
}

.close-x:focus-visible {
    outline: 2px solid var(--c-orange);
    outline-offset: 2px;
}

/* WHOSE BOOK IT IS. data-owner-state="nobody|named" (Zlatan, 2026-09-25):
   arriving with a chart already chosen, the product page says the name
   instead of "you" — the cover is the only thing left to pick. Both halves
   are written in the markup, as with every other state, so no sentence is
   ever assembled from a name in the script. In Rails the name is
   interpolated into the locale string; here it is the sample chart's.
   The state sits on <body> so it reaches the floating bar and the overlays,
   which live outside <article> (2026-09-27: the bar showed both buttons). */
/* A NAME NEEDS A CHART (Zlatan, 2026-10-04). With no saved birth chart
   (data-charts-state="zero") there is no name to say, whatever the owner
   state claims, so "nobody" always wins there. */
[data-owner-state="nobody"] [data-owner-part="named"],
[data-owner-state="named"]:not([data-charts-state="zero"]) [data-owner-part="nobody"],
[data-charts-state="zero"] [data-owner-part="named"] {
    display: none;
}

/* FULL PRICE OR DISCOUNTED. data-price-state="full|sale" (Zlatan,
   2026-10-08): with a discount every price on the product page shows the
   full one struck through beside what is paid, as the landing page's offer
   box does. On <body> for the same reason as the owner state: the floating
   bar and the order panel sit outside <article>. Both prices are in the
   markup; Rails renders the sale half only when the product carries one. */
[data-price-state="full"] [data-price-part="sale"],
[data-price-state="sale"] [data-price-part="full"] {
    display: none;
}

/* The full price, struck through, in front of the discounted one. Sized and
   coloured off the line it sits in (em, currentColor), so the same element
   works on the cream top, on the dark close, in the floating bar and in the
   order panel. The stroke is the landing page's diagonal (.strike-price),
   not text-decoration, which runs through the middle of the digits. */
.price-was {
    position: relative;
    display: inline-block;
    margin-right: .15em;
    font-size: .75em;
    font-family: var(--font-heading);

    /* font-weight: var(--fw-light) */
    text-decoration: none;
    opacity: .7;
}

.price-was::after {
    content: "";
    position: absolute;
    left: -7%;
    right: -10%;
    top: 55%;
    border-top: 2px solid currentColor;
    transform: rotate(-7deg);
    pointer-events: none;
}

/* An overlay takes focus when it opens, so a keyboard is not left behind the
   backdrop. That focus is bookkeeping, not a control being used, so it draws
   no ring. Added 2026-09-25 with the change that moved focus off the close. */
.modal-panel:focus,
.auth-panel:focus,
.order-overlay:focus,
.book3d-overlay:focus,
.gallery-viewer:focus {
    outline: none;
}

/* WHERE IT SITS. The controls this replaced each positioned themselves in
   their own rule; the shared button does not, so the corner is stated once,
   for every panel and full-screen overlay that has one. The gallery viewer is
   deliberately not in the list: its close sits in the viewer's own bar, in
   the flow beside the counter. */
.modal-panel .close-x,
.auth-panel .close-x,
.order-overlay .close-x,
.book3d-overlay .close-x {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    z-index: 2;
}

/* The full-screen overlays are wider than a panel and their close sits
   further in, as .book3d-overlay__close and .order-overlay__close did. */
.order-overlay .close-x,
.book3d-overlay .close-x {
    top: var(--space-4);
    right: var(--space-4);
}

/* THE ONE PLACE currentColor GETS IT WRONG. The 3D overlay is a light room
   on a dark site, so the colour it inherits is the page's white and the X
   all but disappears on cream — measured 1.2:1 the first time this was
   swept in. Its own control stated dark blue; so does this one. */
.book3d-overlay .close-x {
    color: var(--c-blue-900);
}

@media (prefers-reduced-motion: reduce) {
    .close-x::before,
    .close-x::after {
        transition: none;
    }
}

/* ==========================================================================
   Astrology basics — _chapter_list, _chapter_nav, _chapter_text,
   _chapter_pager, _basics_to_book. Added 2026-09-26 with basics.html and
   basics-chapter.html (page list 9.1 and 9.2).
   ========================================================================== */
/* ---- _chapter_list — the chapter cards on basics.html ----
   1, 2, then 3 across, like the catalogue: Rails renders however many
   chapters there are and the grid takes them. An <ol>, because the order is
   the point — the list is read top to bottom. The numbers are printed by
   Rails, so the list's own markers are off. */
.chapter-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
}

@media (min-width: 600px) {
    .chapter-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 900px) {
    .chapter-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

.chapter-grid > li {
    display: flex;
}

/* A dark card on a dark ground, drawn by its seam rather than a fill: the
   7px --c-blue-700 top border is the band seam from the 2026-09-17 pass.
   Square corners, same pass. The whole card is the link, so the tap target is
   the card, not the word "Read". */
.chapter-card {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    padding: var(--space-6);
    background-color: var(--c-blue-850);
    border-top: 7px solid var(--c-blue-700);
    color: var(--c-white-85);
    overflow: clip;
    transition: background-color var(--transition), border-color var(--transition);
    background-image: url('../img/icons/basics-chart.svg');
    background-repeat: no-repeat;
    background-size: 5.25rem;
    background-position: right .5rem top .5rem;
}

.chapter-card:hover {
    background-color: var(--c-blue-900);
    border-top-color: var(--c-orange);
    color: var(--c-white-85);
}

.chapter-card.chapter-02 {
    background-image: url('../img/icons/basics-parts.svg');
}

.chapter-card.chapter-03 {
    background-image: url('../img/icons/basics-planets.svg');
}

.chapter-card.chapter-04 {
    background-image: url('../img/icons/basics-houses.svg');
}

.chapter-card.chapter-05 {
    background-image: url('../img/icons/basics-aspects.svg');
}

.chapter-card.chapter-06 {
    background-image: url('../img/icons/basics-points.svg');
}

/* The glyph is decoration, so it sits large and faint in the corner rather
   than competing with the title. aria-hidden in the markup. */
.chapter-card__glyph {
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    font-size: var(--fs-5xl);
    color: var(--c-white-25);
    transition: color var(--transition);
}

.chapter-card:hover .chapter-card__glyph {
    color: var(--c-orange-50);
}

.chapter-card__number {
    font-weight: var(--fw-bold);
    color: var(--c-orange);
    letter-spacing: 2px;
    margin-bottom: var(--space-2);
}

/* Look stated outright, so the tag can change level (house style). */
.chapter-card__title {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    line-height: var(--lh-tight);
    color: var(--c-white-85);
    margin: 0 0 var(--space-2);
    padding-right: var(--space-12);
}

.chapter-card__text {
    font-weight: var(--fw-light);
    flex-grow: 1;
}

.chapter-card__more {
    font-weight: var(--fw-bold);
    color: var(--c-orange-400);
    transition: color var(--transition);
    text-transform: uppercase;
}

.chapter-card:hover .chapter-card__more {
    color: var(--c-green);
}

/* ---- the chapter page's hero is shorter ----
   The reader picked this chapter a click ago; the text should be close. The
   wide sky file (3661x900) is made for a short band. */
.chapter-hero {
    min-height: 320px;
}

/* ---- .hero-wheel: the astrology basics heroes, list and chapters ----
   The wheel as a background, not an <img>: it is the same on every basics
   page and says nothing. It fills the hero at every size (Zlatan, 2026-09-28:
   contain -> cover, which also retired the phone-only cover rule — contain
   had left a strip with hard edges above and below on a phone). */
.hero-wheel {
    background-image: url('../img/bgr/astro-cosmic-wheel.webp');
    background-size: cover;
    background-position: right center;
    background-repeat: no-repeat;
}

/* ---- _chapter_nav + _chapter_text — the reading layout ----
   Phone: menu above the text, folded. From 900px: menu in a column on the
   left that stays in view, text in a reading measure beside it. */
.chapter-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-8);
}

@media (min-width: 900px) {
    .chapter-layout {
        grid-template-columns: 14rem minmax(0, 1fr);
        gap: var(--space-12);
        align-items: start;
    }

    .chapter-nav {
        position: sticky;
        top: var(--space-8);
    }
}

.chapter-nav__toggle {
    display: none;
    width: 100%;
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--c-white-25);
    background-color: var(--c-blue-850);
    color: var(--c-white-85);
    font-family: var(--font-body);
    font-size: var(--fs-lg);
    text-align: left;
    cursor: pointer;
    background-image: url('../img/icons/chevron-down-white.svg');
    background-repeat: no-repeat;
    background-position: right var(--space-4) center;
    background-size: 1rem;
    padding-right: var(--space-12);
}

.chapter-nav__label {
    display: block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--c-orange);
}

.chapter-nav__current {
    display: block;
    font-weight: var(--fw-semibold);
}

/* The fold exists only once the script has set data-nav-state; without it
   the list stays open and the button stays hidden. 899, not 900, so this
   and the column layout switch on the same pixel (house style). */
@media (max-width: 899px) {
    .chapter-nav[data-nav-state] .chapter-nav__toggle {
        display: block;
    }

    .chapter-nav[data-nav-state="closed"] .chapter-nav__list,
    .chapter-nav[data-nav-state="closed"] .chapter-nav__all {
        display: none;
    }

    .chapter-nav[data-nav-state="open"] .chapter-nav__toggle {
        background-image: url('../img/icons/chevron-down-white.svg');
    }

    .chapter-nav[data-nav-state="open"] .chapter-nav__list {
        border: 1px solid var(--c-white-25);
        border-top: 0;
        padding: var(--space-2) var(--space-4);
    }
}

.chapter-nav__list {
    margin: 0 0 var(--space-4);
    padding: 0;
    list-style: none;
    counter-reset: chapter;
}

.chapter-nav__list li {
    counter-increment: chapter;
    border-bottom: 1px solid var(--c-white-25);
}

.chapter-nav__list li:last-child {
    border-bottom: 0;
}

/* The number comes from the list's own counter here, not from the markup:
   in the menu it is navigation, not content, so nothing to translate. */
.chapter-nav__list a {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    color: var(--c-white-75);
    line-height: var(--lh-snug);
}

.chapter-nav__list a::before {
    content: counter(chapter, decimal-leading-zero);
    font-weight: var(--fw-bold);
    color: var(--c-white-55);
    letter-spacing: 1px;
}

.chapter-nav__list a:hover {
    color: var(--c-green);
}

.chapter-nav__list a[aria-current="page"] {
    color: var(--c-white);
    font-weight: var(--fw-semibold);
}

.chapter-nav__list a[aria-current="page"]::before {
    color: var(--c-orange);
}

.chapter-nav__all {
    display: inline-block;
    font-weight: var(--fw-bold);
}

/* A READING MEASURE. 38em is where a long text stops tiring the eye; the
   rest of the site is short blocks and never needed one. */
.chapter-body {
    max-width: 38em;
    font-size: var(--fs-md);
    font-weight: var(--fw-light);
    margin-left: auto;
    margin-right: auto;
}

.chapter-body p {
    margin-bottom: var(--space-4);
}

.chapter-lead {
    font-size: var(--fs-xl);
    line-height: var(--lh-snug);
    color: var(--c-white);
    margin-bottom: var(--space-8);
}

.chapter-heading {
    font-family: var(--font-heading);
    font-size: var(--fs-3xl);
    font-weight: var(--fw-bold);
    line-height: var(--lh-tight);
    color: var(--c-white-85);
    margin: var(--space-12) 0 var(--space-4);
}

.chapter-body h3 {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    margin: var(--space-12) 0 var(--space-4);
}

.chapter-body b {
    font-weight: var(--fw-semibold);
    color: var(--c-white);
}

@media (max-width: 767px) {
    .chapter-heading {
        font-size: var(--fs-2xl);
        margin-bottom: var(--space-6);

        /* text-align: center */
    }

    .chapter-lead {
        font-size: var(--fs-lg);
    }
}

/* ---- a planet's own heading in the chapter text (2026-09-27) ----
   The glyph in the same ring as the planet list, the name, then what the
   planet stands for, quieter. */
.planet-heading {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;
    column-gap: var(--space-3);
    margin: var(--space-12) 0 var(--space-4);
    font-family: var(--font-heading);
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    line-height: var(--lh-tiny);
    color: var(--c-orange);
}

/* The Sun and the Moon carry their glyph too (Zlatan, 2026-09-28), in the
   same ring, beside a chapter heading. The ring does not shrink and the text
   wraps beside it, not under it: these headings run to two lines. */
.chapter-heading--glyph {
    display: flex;
    align-items: flex-start;
    column-gap: var(--space-4);
}

.chapter-heading--glyph .astro-glyph {
    flex: 0 0 auto;
}

.planet-heading .astro-glyph,
.chapter-heading--glyph .astro-glyph {
    display: inline-flex;
    width: 3.25rem;
    height: 3.25rem;
    border: 1px solid var(--c-orange-50);
    border-radius: 50%;
    font-size: var(--fs-2xl);
    font-weight: var(--fw-regular);
    color: var(--c-orange);
    align-items: center;
    justify-content: center;
}

.planet-heading__point {
    /* font-family: var(--font-body) */
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    color: var(--c-white-75);
    text-transform: uppercase;
}

/* ---- the planet list ----
   Glyph in a ring, name, then Marsa's keywords. One per row: the keywords
   run long, and two columns would break them at every width. */
.planet-list {
    margin: 0 0 var(--space-4);
    padding: 0;
    list-style: none;
}

.planet-list li {
    display: grid;
    grid-template-columns: 2.75rem minmax(0, 1fr);
    column-gap: var(--space-4);
    align-items: center;
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--c-white-25);
}

.planet-list li:last-child {
    border-bottom: 0;
}

.planet-list .astro-glyph {
    grid-row: span 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border: 1px solid var(--c-orange-50);
    border-radius: 50%;
    font-size: var(--fs-xl);
    color: var(--c-orange);
}

.planet-list b {
    display: block;
    line-height: var(--lh-tight);
}

/* ---- _chapter_pager — previous and next ---- */
.chapter-pager {
    display: flex;

    /* NO WRAP, 2026-10-02 (Zlatan). The two buttons used to wrap onto a second
       row as soon as their combined width passed the container: chapter 02 at
       600px needs 230 + 333 + a 16px gap = 579px and has 576, so "The Sun, the
       Moon and the planets" dropped under the other button. They shrink and
       break the label onto a second line instead (min-width: 0 on the items),
       so the pager stays one row at every width. */
    flex-wrap: nowrap;
    justify-content: space-between;
    gap: var(--space-4);
    margin-top: var(--space-12);
    padding-top: var(--space-8);
    border-top: 1px solid var(--c-white-25);
}

.chapter-pager__item {
    display: flex;
    min-width: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
}

.chapter-pager__item--next {
    margin-left: auto;
    align-items: flex-end;
    text-align: right;
}

.chapter-pager__label {
    font-size: var(--fs-xs);
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--c-white-55);
}

/* An outline button with the site's white chevron on the side it points to. */
.chapter-pager__button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
}

.chapter-pager__button img {
    flex: none;
}

/* ---- the calculation note ----
   A fact for the reader who wants to know the method, set quietly: it answers
   a question, it does not make a claim. UNUSED since 2026-09-27: it left the
   basics chapter list and waits for the about-the-reading page. */
.calc-note {
    margin-top: var(--space-8);
    font-size: var(--fs-sm);
    color: var(--c-white-55);
}

/* ---- _basics_to_book ---- */
.basics-book__row {
    gap: var(--space-8);
    align-items: center;
}

.basics-book__row img {
    width: 100%;
}

@media (max-width: 767px) {
    .basics-book__row .col-48 {
        width: 100%;
    }

    .basics-book__row .col-48:first-child {
        order: 1;
    }
}

@media (min-width: 768px) {
    .basics-book__row {
        flex-wrap: nowrap;
    }
}

/* The chart floats (Zlatan, 2026-10-09: "add some animation, rotating?").
   Not a full turn: the wheel is drawn tilted, so spinning the picture would
   spin the tilt with it and the plate would tumble. Instead it sways a few
   degrees and lifts a little, slowly enough to be felt rather than watched. */
.basics-book__chart {
    animation: basics-chart-float 18s ease-in-out infinite alternate backwards;
}

@keyframes basics-chart-float {
    from {
        transform: rotate(-3deg) translateY(0);
    }

    to {
        transform: rotate(3deg) translateY(-12px);
    }
}

@media (prefers-reduced-motion: reduce) {
    .basics-book__chart {
        animation: none;
    }
}

/* ==========================================================================
   _cookie_consent — Klaro, in the site's own look (2026-10-08)
   Klaro draws the notice and the choices and ships its own stylesheet; it
   is restyled here through the variables it reads, so no rule of Klaro's
   is copied. The words come from [data-consent-copy] (js/consent.js).

   KLARO ADDS ITS OWN STYLESHEET AT RUN TIME, AFTER THIS ONE, so a rule of
   equal weight here loses on order. The colours go through its variables
   (set on .klaro, which it only reads); every other rule starts at #klaro,
   the container our config names (elementID), and so outweighs its
   class-only selectors without !important.

   Dark, like every overlay for reading (dark-design rule); corners square;
   the site's buttons: accept is the orange-red primary, decline the outline
   — same size, side by side, so saying no takes exactly what yes takes.
   ========================================================================== */
.consent-copy {
    display: none;
}

.klaro {
    --font-family: var(--font-body);
    --title-font-family: var(--font-heading-fat);
    --font-size: var(--fs-sm);
    --border-radius: 0;
    --dark1: var(--c-blue-900);
    --dark2: var(--c-blue-700);
    --dark3: var(--c-white-75);
    --light1: var(--c-white-85);
    --light2: var(--c-white-25);
    --light3: var(--c-white-55);
    --green1: var(--c-orange);
    --green2: var(--c-green);
    --green3: var(--c-orange);
    --blue1: var(--c-blue-700);
    --white2: var(--c-blue-700);
    --white3: var(--c-white-85);
    --button-text-color: var(--c-white-soft);
    --box-shadow-color: var(--overlay-scrim);
}

#klaro .cookie-notice:not(.cookie-modal-notice),
#klaro .cookie-modal .cm-modal.cm-klaro {
    border-top: 7px solid var(--c-blue-700);
    box-shadow: var(--shadow-raised);
}

#klaro .cookie-modal .cm-modal .cm-header h1.title {
    font-family: var(--font-heading-fat);
    font-size: var(--fs-2xl);
}

/* The site's buttons. Klaro's own are small and rounded; these keep its
   layout and take ours. */
#klaro .cm-btn {
    padding: var(--space-2) var(--space-4);
    border-radius: 0;
    font-family: var(--font-body);
    letter-spacing: 1px;
    text-transform: uppercase;
    transition: background-color var(--transition);
}

#klaro .cm-btn.cm-btn-success {
    background-color: var(--c-orange-red);
}

#klaro .cm-btn.cm-btn-decline,
#klaro .cm-btn.cn-decline,
#klaro .cm-btn.cm-btn-info {
    box-shadow: inset 0 0 0 2px var(--c-white-75);
    background-color: transparent;
    color: var(--c-white-85);
}

#klaro .cm-btn:hover,
#klaro .cm-btn:focus-visible {
    background-color: var(--c-green);
    box-shadow: none;
    color: var(--c-white);
}

/* "Choose" on the notice is a link in the accent, not a third button. */
#klaro .cookie-notice .cn-learn-more {
    color: var(--c-orange);
    text-decoration: underline;
}

/* Klaro's "↓" before "1 service" becomes the site's one down arrow, the
   thin chevron (house style, 2026-09-30), turned when the list is open. */
#klaro .cm-caret a > span {
    display: inline-block;
    width: .75rem;
    height: .75rem;
    margin-right: var(--space-1);
    font-size: 0;
    background: url('../img/icons/chevron-down-white.svg') no-repeat center / contain;
    vertical-align: middle;
    transition: transform var(--transition);
}

#klaro .cm-caret a[aria-expanded="true"] > span {
    transform: rotate(180deg);
}

/* ==========================================================================
   Overrides — last in the file because they have to win
   ========================================================================== */
/* MOVED: var(--space-12) 0`. Both are single-class selectors,
   so the later one won and the utility silently did */
.padding-top-0 {
    padding-top: 0;
}

#covers-section {
}

.img-divider {
    /* max-height: 4vw */
    /* height: 4vw */
    width: 3vh;
    max-width: 3vh;
    margin: var(--space-8) auto;
    opacity: .75;
}

.orange-txt {
    color: var(--c-orange);
}

.card-label.blue800 {
    background-color: var(--c-blue-800);
}

.card-label.blue500 {
    background-color: var(--c-blue-500);
}

/* A second label stacks under the first. Found by position, not by a class
   (Zlatan, 2026-09-18): `~` matches any .card-label with another one before
   it in the same card, so the markup only says which labels a card has, and a
   card with a badge but no price still puts that badge at the top. */
.card-label ~ .card-label {
    top: 4rem;
}

.card-label.orangered {
    background-color: var(--c-orange-red);
}

.hero-line-bottom {
    /* height: 7px */
    background-color: var(--c-blue-850);
    max-height: 7px;
    min-height: 7px;
    z-index: 2;
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
}

/* Section grounds — each class is named after the colour it paints
   (straightened 2026-09-27: 850 and 800 had their colours crossed, and both
   painted the same 800 gradient, since the image always covers the colour).
   Every section that had either class now has bgr-blue-800, so the pages look
   exactly as before; bgr-blue-850 is the true 850 ground, free to use. */
.bgr-blue-800 {
    background-color: var(--c-blue-800);
    background-image: var(--c-blue-gradient-800);
}

.bgr-blue-850 {
    background-color: var(--c-blue-850);
    background-image: var(--c-blue-gradient-900);
}

.bgr-blue-900 {
    background-color: var(--c-blue-900);
    background-image: var(--c-blue-gradient-950);
}

.bgr-cream {
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream);
}

.bgr-tan {
    background-color: var(--c-tan);
    background-image: var(--c-gradient-cream);
}

.bgr-cream-rev {
    background-color: var(--c-cream);
    background-image: var(--c-white-gradient-cream-rev);
}

#chart-to-book .row {
    /* align-items: center */
}

@media (max-width:767px) {
    #chart-to-book .col-48:first-child {
        order: 1;
    }
}

.no-border {
    border: none !important;
}

/* dl.chart-facts dd {
    display: block;
} */
/* The gift voucher gets its picture (Zlatan, 2026-09-24). Beside the words
   from 600px, above them on a phone — the card is a short offer, not a
   section, so the image stays small enough to read as an illustration. */
.coupon-row {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-4);
    margin-top: var(--space-12);
    margin-bottom: var(--space-12);
}

.coupon-image {
    width: 100%;
    max-width: 11rem;
    height: auto;
}

@media (min-width: 600px) {
    .coupon-row {
        flex-direction: row;

        /* text-align: left */
        gap: var(--space-4);
    }

    .coupon-image {
        flex: 0 0 auto;
        max-width: 9rem;
    }
}

.bgr-flat {
    background-image: none;
}

.floating-cta img {
    height: 2rem;
    margin: .85rem 1.25rem;
}

.archive-07-removals {
    display: none !important;
}

.dark-text {
    color: var(--c-text);
}

#order-overlay.auth-panel,
#chart-confirm-overlay.auth-panel {
    max-height: 96%;
    top: 2%;
}

@media (max-width:599px) {
    #order-overlay.auth-panel,
    #chart-confirm-overlay.auth-panel {
        padding: var(--space-8) var(--space-6);
    }
}

#confirm-data .cart-line__actions {
    justify-content: center;
}

#confirm-data .dedication {
    margin-top: var(--space-2);
}

#chapter-text .block-star {
    margin: 1.75rem auto 1rem;
    max-width: 1.5rem;
}

@media (min-width:768px) {
    .mobile-767 {
        display: none !important;
    }
}

@media (max-width:767px) {
    .desktop-768 {
        display: none !important;
    }

    .col-48-margin {
        margin-top: var(--space-8);
    }
}

/* REVIEW ONLY, 2026-09-28 — remove after the text pass (basics chapters, then
   every page and email the same day).
   <mark class="review-flag" title="why"> highlights wording that reads as
   machine-written; the reason is in the title. Delete this rule and every
   review-flag <mark> together (grep review-flag). */
mark.review-flag {
    background-color: rgba(255, 243, 116, 0.9);
    color: inherit;

    /* border-bottom: 1px dotted var(--c-orange) */
    cursor: help;

    /* background: none */
}

@media (max-width:767px) {
    .basics-col-48-image {
        order: 1;
        margin-top: var(--space-6);
    }
}

/* ONE WAY TO SHOW A SAVED CHART (Zlatan, 2026-10-01: "unify charts select
   lists and buttons: emphasized name, add birth day; both bigger than the
   title"). In every list and on every Use / Open button: the name bold and
   large, the birth date after it at the regular size — both larger than the
   small "Use this birth chart" label above them. The ID rule was his; the
   class version carries it to the free-chart forms' lists. */
ul#chart-list-menu a b,
ul.chart-list li ul a b,
.saved-chart__name b {
    font-size: var(--fs-lg);
    font-weight: var(--fw-bold);
}

ul.chart-list li ul a,
.saved-chart__name {
    font-size: var(--fs-base);
    font-weight: var(--fw-regular);

    /* Names as they were typed, never in capitals (Zlatan, 2026-10-01: "not
       uppercase") — the lists' wrapper and <li> set uppercase for the title,
       and the entries take it off again. */
    text-transform: none;
}

.review-order .section-label b {
    margin-top: 0.5rem;
    display: block;
    font-size: var(--fs-lg);
    color: var(--c-text);
}

#our-exact.proof-grid {
    /* background-color: var(--c-blue-850) */
    /* background-image: url('../img/bgr/tile-moon-white-5.webp') */
    /* background-size: 100vw */
}

#our-closing p {
    font-size: var(--fs-3xl);

    /* color: var(--c-orange) */
    font-family: var(--font-heading);
}

.border-top-1 {
    border-top: 1px solid var(--c-white-25);
    padding-top: var(--space-8);
}

@media (max-width:599px) {
    .cart-promise-div {
        text-align: center;
    }
}

.celeb-explorer {
    background-color: var(--c-blue-950);
}

#gift-voucher-section {
    background-color: var(--c-blue-950);
    display: flex;
    gap: 0px;
    align-items: stretch;
    justify-content: space-around;
    flex-basis: 50%;
    flex-grow: 1;
    flex-shrink: 1;
    border-top: 7px solid var(--c-blue-700);
    border-bottom: 7px solid var(--c-blue-700);

    /* flex-wrap: wrap */
    /* min-height: 300px */
}

@media (max-width:767px) {
    #gift-voucher-section {
        flex-wrap: wrap;
    }
}

.gv-col-1 {
    width: 50%;
    min-height: 300px;
    background-image: url('../img/photos/gift-voucher.webp');
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center center;
    margin-right: 0.75em;
}

@media (max-width:767px) {
    .gv-col-1 {
        width: 100%;
        min-height: 250px;
        margin-right: 0;

        /* margin-bottom: var(--space-8) */
    }
}

.voucher-offer-div {
    min-height: 17rem;
    background-image: url('../img/photos/gift-voucher.webp');
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center center;
}

@media (min-width:768px) {
    .voucher-offer-div {
        min-height: 10rem;
        background-image: url('../img/photos/gift-voucher-square.webp');
    }
}

#redeem-help {
    background-color: var(--c-blue-950);
}

@media (max-width:767px) {
    section#faq {
        text-align: left;
    }
}

/* .chart-tile.purchased and the primary address moved to css/account.css
   2026-10-07: they are My astro's alone. Zlatan's #password moved there too;
   .mr-1 sits with the other margin utilities. */
.reading-contents__group {
    border-top: 1px dotted var(--c-orange);
    padding-top: var(--space-6);
}

section#company {
    background-image: url('../img/bgr/space-planets.webp');
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center bottom;
    background-color: var(--c-blue-950);
}

