/* ==========================================================================
   Jackpot Compare — site.css
   Modern, responsive, dependency-free. Brand colours from the original site:
   blue #0065b3, green #7cda24, dark footer #171717. Typeface: Inter (Google
   Fonts, loaded from layout.php) with a system-font fallback stack.

   CONVERSION PASS (see docs/CONVERSION-NOTES.md)
   ---------------------------------------------
   One rule governs most of the colour decisions below: GREEN IS RESERVED FOR
   OUTBOUND PRIMARY CTAs (the /go/ buttons). Nothing decorative is allowed to
   be green any more — not the nav underline, not the "New" badge, not the
   heading rules — because a colour that appears everywhere can't mean
   anything. Blue carries all decorative/brand weight; green means "this is
   the button that takes you to the operator".

   The button ladder is therefore three rungs, and a card/box may only ever
   have ONE of the top two on screen at a time:
     .btn--primary  green  — outbound offer link (/go/{slug}/)
     .btn--solid    blue   — dominant ON-SITE action, used when there is no
                             outbound link yet (affiliate_url still NULL), so
                             a card is never left with only a ghost button
     .btn--outline  ghost  — genuine secondary
   ========================================================================== */

:root {
    /* Brand */
    --brand-blue: #0065b3;
    --brand-blue-dark: #004a85;
    --brand-blue-light: #eaf4fc;
    --brand-green: #7cda24;
    --brand-green-dark: #5fae18;
    --brand-dark: #171717;

    /* CTA ink — deep brand blue rather than --brand-blue-dark so that BOTH
       the resting green (#7cda24, 7.7:1) and the hover green (#5fae18,
       4.9:1) clear WCAG AA for normal-size bold text. Using
       --brand-blue-dark here would drop the hover state to 3.2:1. */
    --cta-ink: #00304f;

    /* Neutrals */
    --text: #1c232c;
    --text-muted: #5a6472;
    --border: #e1e6ec;
    --border-strong: #c7cfd9;
    --bg: #ffffff;
    --bg-alt: #f5f7fa;

    /* Legacy/archived-content notice */
    --amber-bg: #fdf3df;
    --amber-border: #e8c77a;
    --amber-text: #6b4e10;

    /* Ratings */
    --star: #f5a524;
    --star-track: #dfe3e8;

    /* Shape + elevation */
    --radius-sm: 8px;
    --radius: 14px;
    --radius-lg: 20px;
    --max-width: 1180px;
    --shadow-sm: 0 1px 3px rgba(15, 23, 42, .07);
    --shadow: 0 6px 18px rgba(15, 23, 42, .09);
    --shadow-lg: 0 16px 36px rgba(15, 23, 42, .14);

    --focus-ring: #0065b3;
    --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

    /* Minimum comfortable touch target. Majority of this vertical's traffic
       is mobile, and every CTA is a thumb target — nothing interactive is
       allowed below this on small screens. */
    --tap: 48px;
}

* { box-sizing: border-box; }

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

body {
    margin: 0;
    font-family: var(--font-sans);
    color: var(--text);
    background: var(--bg);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
    line-height: 1.22;
    color: var(--text);
    font-weight: 800;
    letter-spacing: -.01em;
    margin: 0 0 .5em;
}
h1 { font-size: clamp(1.9rem, 4vw, 2.6rem); }
h2 { font-size: clamp(1.35rem, 3vw, 1.75rem); }
h3 { font-size: 1.15rem; }

p { margin: 0 0 1em; }

a { color: var(--brand-blue); text-decoration: none; }
a:hover { text-decoration: underline; }

img { max-width: 100%; height: auto; display: block; }

ul, ol { padding-left: 1.25em; }

.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* Skip link — lets a keyboard user jump the nav straight to the cards. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    background: #fff;
    color: var(--brand-blue-dark);
    font-weight: 700;
    padding: 12px 18px;
    border-radius: 0 0 var(--radius-sm) 0;
    box-shadow: var(--shadow);
}
.skip-link:focus { left: 0; }

/* The header is sticky, so an in-page anchor would otherwise land underneath
   it. */
#main-content { scroll-margin-top: 84px; }

/* Focus states -------------------------------------------------------- */
a:focus-visible,
button:focus-visible,
.btn:focus-visible,
.nav-toggle:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 3px;
    border-radius: 2px;
}

.wrap {
    max-width: var(--max-width);
    margin: 0 auto;
    padding: 0 20px;
}

/* Buttons ---------------------------------------------------------------- */
/* min-height on the base class (not just the mobile breakpoint): a CTA that
   is only thumb-sized below 780px is still a bad target on a 900px-wide
   tablet held in two hands. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap);
    font-weight: 700;
    text-align: center;
    padding: 12px 24px;
    border-radius: 999px;
    border: 2px solid transparent;
    cursor: pointer;
    text-decoration: none;
    font-size: .95rem;
    line-height: 1.25;
    transition: transform .12s ease, background-color .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
/* Press feedback. A CTA that doesn't visibly respond to the press reads as
   broken on touch, where there is no hover state to confirm the hit. */
.btn:active { transform: translateY(1px) scale(.985); }

/* Rung 1 — outbound offer only. Green appears nowhere else in this file. */
.btn--primary {
    background: var(--brand-green);
    color: var(--cta-ink);
    border-color: var(--brand-green);
    box-shadow: 0 2px 10px rgba(124, 218, 36, .35);
}
.btn--primary:hover {
    background: var(--brand-green-dark);
    border-color: var(--brand-green-dark);
    box-shadow: 0 6px 18px rgba(95, 174, 24, .42);
}

/* Rung 2 — dominant on-site action. Carries the same visual weight as a
   primary (solid fill, same size) but in blue, so a card whose operator has
   no affiliate link yet still has one unmistakable thing to click, without
   borrowing the meaning of "go to the operator". */
.btn--solid {
    background: var(--brand-blue);
    color: #fff;
    border-color: var(--brand-blue);
    box-shadow: 0 2px 10px rgba(0, 101, 179, .28);
}
.btn--solid:hover {
    background: var(--brand-blue-dark);
    border-color: var(--brand-blue-dark);
    box-shadow: 0 6px 18px rgba(0, 74, 133, .34);
}

/* Rung 3 — genuine secondary. */
.btn--outline {
    background: transparent;
    color: var(--brand-blue);
    border-color: var(--border-strong);
}
.btn--outline:hover { background: var(--brand-blue); color: #fff; border-color: var(--brand-blue); }
.btn--on-dark.btn--outline { color: #fff; border-color: rgba(255,255,255,.7); }
.btn--on-dark.btn--outline:hover { background: #fff; color: var(--brand-blue-dark); border-color: #fff; }
/* Blue-on-blue would vanish against the hero's dark overlay, and reaching
   for green here would spend the one colour that has to keep meaning
   "outbound to the operator". White is the highest-contrast thing available
   on that background — 9:1 against the ink — and stays inside the palette. */
.btn--on-dark.btn--solid {
    background: #fff;
    color: var(--brand-blue-dark);
    border-color: #fff;
    box-shadow: 0 4px 16px rgba(0, 0, 0, .22);
}
.btn--on-dark.btn--solid:hover {
    background: var(--brand-blue-light);
    border-color: var(--brand-blue-light);
    color: var(--brand-blue-dark);
    box-shadow: 0 8px 22px rgba(0, 0, 0, .28);
}
.btn--block { display: flex; width: 100%; }
.btn--lg { font-size: 1.05rem; padding: 16px 34px; min-height: 56px; }

.link-more { font-weight: 700; white-space: nowrap; color: var(--brand-blue); }

/* Respect the OS "reduce motion" setting — the lift/press transforms are
   decoration, the colour changes carry the actual state information. */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
    .btn:hover, .btn:active, .site-card:hover { transform: none; }
}

/* Star rating (CSS-only partial fill) ------------------------------------ */
.stars {
    --rating: 0%;
    position: relative;
    display: inline-block;
    font-size: 1rem;
    line-height: 1;
    letter-spacing: 2px;
    vertical-align: middle;
}
.stars__track,
.stars__fill {
    display: block;
    white-space: nowrap;
}
.stars__track { color: var(--star-track); }
.stars__fill {
    position: absolute;
    inset: 0;
    overflow: hidden;
    width: var(--rating);
    color: var(--star);
}

/* Header ------------------------------------------------------------------ */
.site-header {
    background: #fff;
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-sm);
    position: sticky;
    top: 0;
    z-index: 30;
}
/* Brand hairline. Was blue→green; now blue→light-blue. Green at the very top
   of every page trained the eye to read green as "chrome", which is exactly
   what we need it NOT to mean. */
.site-header::before {
    content: "";
    display: block;
    height: 3px;
    background: linear-gradient(90deg, var(--brand-blue-dark), var(--brand-blue) 55%, #4aa3e0);
}
.site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: 14px;
    padding-bottom: 14px;
    gap: 16px;
}
.site-header__logo { display: inline-flex; }
.site-header__logo img { height: 38px; width: auto; }

/* 44x44 minimum: this was a 36px-tall button and it is the only way to reach
   the nav on a phone. */
.nav-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 1.4rem;
    line-height: 1;
    padding: 6px 12px;
    cursor: pointer;
    color: var(--text);
}
.nav-toggle:hover { border-color: var(--brand-blue); color: var(--brand-blue); }

.site-nav ul {
    list-style: none;
    display: flex;
    gap: 26px;
    margin: 0;
    padding: 0;
}
.site-nav a {
    position: relative;
    display: inline-block;
    color: var(--text);
    font-weight: 600;
    font-size: .95rem;
    padding-bottom: 4px;
}
.site-nav a::after {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: -3px;
    height: 2px;
    background: var(--brand-blue);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .15s ease;
}
.site-nav a:hover { color: var(--brand-blue); text-decoration: none; }
.site-nav a:hover::after,
.site-nav a:focus-visible::after { transform: scaleX(1); }

@media (max-width: 780px) {
    .nav-toggle { display: inline-flex; }
    .site-nav {
        width: 100%;
        order: 3;
        max-height: 0;
        overflow: hidden;
        transition: max-height .25s ease;
    }
    .site-nav.is-open { max-height: 480px; }
    .site-nav ul {
        flex-direction: column;
        gap: 0;
        border-top: 1px solid var(--border);
        margin-top: 12px;
    }
    .site-nav li { border-bottom: 1px solid var(--border); }
    .site-nav a { display: block; padding: 13px 4px; }
    .site-nav a::after { display: none; }
}

/* Hero ---------------------------------------------------------------- */
/* Was a fixed 76/68px. On a 667px-tall phone that hero plus the sticky
   header consumed the entire first viewport, so the visitor's first scroll
   happened before they had seen a single bingo site. Fluid padding pulls the
   top of the first card row up into view on both phone and laptop — the
   "visual path to the money pages" is simply making the money visible. */
.hero {
    background-color: var(--brand-blue-dark);
    background-size: cover;
    background-position: center;
    color: #fff;
    padding: clamp(34px, 6vw, 64px) 0 clamp(30px, 5vw, 52px);
    text-align: center;
}
.hero__inner { text-align: center; }
.hero h1 { color: #fff; margin-bottom: .35em; }
.hero__tagline {
    font-size: clamp(1rem, 2.4vw, 1.15rem);
    max-width: 620px;
    margin: 0 auto 1.35em;
    color: rgba(255,255,255,.92);
}

/* ONE primary. The old hero had a green button and an equally sized outline
   button side by side — two co-equal choices is a decision, and a decision
   costs clicks. The second destination is still one tap away, as a text
   link that reads as "and also", not "or". */
.hero__ctas {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
}
.hero__ctas .btn { max-width: 100%; }
.hero__secondary {
    color: #fff;
    font-weight: 600;
    font-size: .95rem;
    text-decoration: underline;
    text-underline-offset: 4px;
    text-decoration-color: rgba(255,255,255,.45);
}
.hero__secondary:hover { color: #fff; text-decoration-color: #fff; }

/* Freshness signal, rendered from the newest bingo_sites.updated_at in the
   rows the page already selected — a real timestamp, not a "checked daily"
   claim. Sits directly under the CTA because that is where the "is this
   current?" objection actually fires. */
.hero__updated {
    margin: 12px 0 0;
    font-size: .8rem;
    color: rgba(255,255,255,.8);
}

.hero__trust {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 26px;
    margin: 24px 0 0;
    padding: 0;
    font-size: .86rem;
    font-weight: 600;
    color: rgba(255,255,255,.95);
}
.hero__trust li { display: flex; align-items: center; gap: 8px; }
.hero__trust-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px; height: 20px;
    border-radius: 50%;
    background: rgba(255,255,255,.92);
    color: var(--brand-blue-dark);
    font-size: .68rem;
    font-weight: 800;
    flex-shrink: 0;
}

@media (max-width: 460px) {
    /* Full-bleed CTA in the thumb zone rather than a pill floating
       mid-screen. */
    .hero__ctas .btn { width: 100%; }
    .hero__trust {
        flex-direction: column;
        align-items: flex-start;
        text-align: left;
        gap: 8px;
        max-width: 20rem;
        margin-inline: auto;
    }
}

/* Sections ------------------------------------------------------------ */
/* Listing header. Trimmed from 52/44 for the same reason as the hero — on a
   listing page the cards ARE the content, and every pixel of header pushes
   the first comparison below the fold. */
.page-header {
    background: linear-gradient(180deg, var(--brand-blue-light), var(--bg-alt));
    padding: clamp(28px, 4.5vw, 44px) 0 clamp(24px, 4vw, 36px);
    border-bottom: 1px solid var(--border);
}
.page-header h1 { margin-bottom: .3em; }
.page-header__intro { color: var(--text-muted); max-width: 70ch; font-size: 1.05rem; }
.page-header__intro p:last-child { margin-bottom: 0; }
/* Freshness line on listing pages, from the newest bingo_sites.updated_at in
   the rows already fetched — same real-timestamp rule as the hero. */
.page-header__updated {
    margin: 12px 0 0;
    font-size: .82rem;
    color: var(--text-muted);
    font-weight: 600;
}

.page-intro { padding: 36px 0 8px; }

/* Legacy migrated copy on a listing page, rendered BELOW the card grid as
   secondary SEO/reference content — deliberately de-emphasised (muted
   colour, tighter measure, top rule) so it reads as supplementary, never
   competing with the grid above it for attention. See
   LISTING_LEGACY_BODY_MODE in ListingController.php. */
.listing-legacy-copy {
    padding: 8px 0 48px;
    margin-top: 8px;
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    max-width: 70ch;
    font-size: .95rem;
}
.listing-legacy-copy h2,
.listing-legacy-copy h3 { color: var(--text-muted); font-size: 1.05rem; }
.listing-legacy-copy a { color: var(--brand-blue); }

/* Tightened from a flat 52px so the first row of cards clears the fold
   sooner; the section still breathes at desktop width. */
.home-section { padding: clamp(30px, 5vw, 48px) 0; }
.home-section--alt { background: var(--bg-alt); border-radius: var(--radius-lg); }
.home-section__header {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 16px;
    margin-bottom: 26px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--border);
}
.home-section__header h2 { margin-bottom: 0; }

.content-page { padding: 44px 0 64px; }
.content-page__header { margin-bottom: 22px; }

/* Legacy/archived-content notice — muted amber, distinct from both the
   brand blue/green and any error/warning red, so it reads as "heads up,
   this is old" rather than "something's broken". Sits directly under the
   H1 on any page/review imported from the old WordPress site. */
.legacy-notice {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    background: var(--amber-bg);
    border: 1px solid var(--amber-border);
    border-left: 4px solid var(--amber-border);
    border-radius: var(--radius);
    padding: 16px 18px;
    margin: 16px 0 28px;
}
.legacy-notice::before {
    content: "\23F3";
    font-size: 1.1rem;
    line-height: 1.4;
    flex-shrink: 0;
}
.legacy-notice p {
    margin: 0;
    color: var(--amber-text);
    font-size: .95rem;
    line-height: 1.55;
}
.legacy-notice a {
    color: var(--amber-text);
    text-decoration: underline;
    font-weight: 700;
}
.legacy-notice a:hover { color: var(--brand-blue-dark); }

.content-page__meta { color: var(--text-muted); font-size: .9rem; }
.content-page__body { max-width: 72ch; }
/* Was a green rule — de-greened along with everything else decorative. */
.content-page__body h2 {
    margin-top: 1.5em;
    padding-bottom: .3em;
    border-bottom: 3px solid var(--brand-blue);
    display: inline-block;
}
.content-page__body table { width: 100%; border-collapse: collapse; margin: 1em 0; }
.content-page__body th, .content-page__body td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; }

/* Legacy body content ------------------------------------------------------
   The 129 imported WordPress pages/reviews (tools/import_wp.php strips the
   old Divi grid markup, so what's left is a flat run of headings, paragraphs
   and images — no columns, no cards, none of the structure that used to
   carry the visual design). `.legacy-body` is added ONLY on rows where
   pages.is_legacy is truthy (see page.php / review.php) — it's a defensive
   "make arbitrary old WordPress content look decent" layer that a future
   hand-authored page/review should never inherit.

   Studied against the real thing, not guesses: fetched the live
   /best-bingo-sites/ (turned out to be the new listing.php grid, not
   legacy — the URL now serves fresh content), /bingo-bonus-codes/
   (page.php, is_legacy) and /bingo-cams-review/ (review.php, is_legacy),
   and grepped the actual deployed dist/install-full.sql for every pattern
   below to get real counts rather than assumptions.

   What's actually in there (counts across the 129 rows, dist/install-full.sql):
     - 1232 inline style="text-align: center;" occurrences — Divi centred
       nearly every paragraph and heading indiscriminately.
     - 348 <div class="offer--headline"> / 288 <div class="offer--terms"> —
       these ARE real surviving class names from the old theme's shortcode
       output (not invented here): logo image, then a headline div (site
       name + bonus headline as h2/h3/h4, inconsistently levelled and
       sometimes nested 2-3 divs deep), then a terms div (small-print +
       "Read Our X Review"/"VISIT SITE" link). This is the "collapsed offer
       table" the brief describes, and it's the single most common pattern.
     - A large number of offer entries have NO wrapper div at all — just a
       bare img, then a run of adjacent h2/h3/h4 (one broken headline split
       across multiple heading tags), then a terms <p>. Handled below by
       spacing + de-duplicating the heading treatment rather than a card,
       since there's no shared element to hang a box on.
     - 101/129 rows start with a literal <h1> that duplicates the page
       title already rendered by page.php/review.php's own H1 — a visible
       double-heading on ~78% of legacy pages. Hidden (not deleted): this is
       a presentation fix, the real fix belongs in the importer.
     - 105 <h6>Last Modified: </h6> (empty date — the WP dynamic-date
       token never resolved) / 1 "Written by NAME | Last Modified: ".
     - 3 real <table> elements survive with inline pixel widths on cells.
     - 1 page contains a leftover <style> block + markup for a defunct
       "AddToAny" floating share widget (`.at-about-fab`), authored with
       position:fixed and z-index:999999. Because it's echoed raw into the
       DOM, that <style> tag applies SITE-WIDE, not just within this
       article. That specific row (slug 'best-bingo-codes') is currently an
       orphaned duplicate that 301-redirects away before ever rendering
       (sql/seed_redirects.sql), so it isn't the confirmed cause of the
       owner's "header floating over content" screenshot — but the same
       leftover-widget pattern could exist on a row that IS reachable, and
       once it is, this is exactly what it would do. Neutralised
       unconditionally below regardless.
     - 11 leftover `@ET-DC@<base64>@` Divi dynamic-content shortcode blobs
       that render as literal gibberish text. This is a content/import bug
       (the shortcode never got resolved or stripped) with no CSS fix —
       flagged separately, not touched here.
   ========================================================================== */

.legacy-body { line-height: 1.65; }
.legacy-body > *:first-child { margin-top: 0; }

/* Duplicate H1: page.php/review.php already render the page title as the
   real H1 above this block, so a leftover H1 at the very top of the
   imported body is always a repeat of it (verified: true for every sampled
   row). Hidden rather than removed — the fix that also fixes the DOM belongs
   in the importer, not here. */
.legacy-body > h1:first-child { display: none; }

/* Measured, not assumed: 11 pages still contain a literal, unresolved Divi
   dynamic-content shortcode — a ~600-character token with no spaces
   (`@ET-DC@<base64>@`). As plain text with no break opportunity, that one
   "word" was stretching its whole container (and the article around it)
   well past the viewport, forcing a page-wide horizontal scrollbar — found
   by measuring .legacy-body's scrollWidth against its clientWidth in a
   local render of the real content, not a visual guess. Any future
   long unbroken token (a raw URL pasted as text, another shortcode leftover)
   would do the same, so this is applied to the whole scope rather than only
   the one known offender. The shortcode text itself is a content/import bug
   or Sam's importer — this only stops it from breaking the layout. */
.legacy-body { overflow-wrap: anywhere; word-break: break-word; }

/* Rhythm --------------------------------------------------------------- */
.legacy-body p { margin: 0 0 1.15em; }
.legacy-body ul, .legacy-body ol { margin: 0 0 1.3em; padding-left: 1.4em; }
.legacy-body li { margin-bottom: .4em; }
.legacy-body li:last-child { margin-bottom: 0; }
.legacy-body blockquote {
    margin: 1.5em 0;
    padding: 2px 20px;
    border-left: 4px solid var(--brand-blue-light);
    color: var(--text-muted);
    font-style: italic;
}
.legacy-body blockquote p:last-child { margin-bottom: 0; }

/* h6 "Last Modified" / byline remnants — often left with no date attached
   at all (the WP token that should have filled it in never resolved), so
   the honest fix here is "make it read as a quiet label", not to fabricate
   a date CSS doesn't have. */
.legacy-body h6 {
    margin: 0 0 1.4em;
    font-size: .78rem;
    font-weight: 600;
    color: var(--text-muted);
}

/* Adjacent headings, with nothing between them, are overwhelmingly one
   broken headline split across multiple tags by the old editor (e.g. "Play
   With £50" as one h2 immediately followed by "+ 50 FREE Spins" as another)
   rather than two real section titles back to back. Two effects follow:
   pull them close together, and drop the branded blue-underline treatment
   from `.content-page__body h2` off of every fragment in the run — a run of
   3-4 individually-underlined half-headlines is exactly the "unstyled
   stacked fragments" the relaunch is meant to fix. A heading with clear air
   on both sides keeps the normal branded treatment. */
.legacy-body :is(h2, h3, h4) + :is(h2, h3, h4),
.legacy-body :is(h2, h3, h4):has(+ :is(h2, h3, h4)) {
    border-bottom: 0;
    display: block;
    padding-bottom: 0;
    margin-top: .15em;
    margin-bottom: .15em;
}

/* An image that follows body copy reads as "a new operator entry starts
   here" in this content (every offer block opens with the operator logo) —
   give it a real section break instead of butting straight into the
   previous paragraph. */
.legacy-body p + img,
.legacy-body ol + img,
.legacy-body ul + img {
    margin-top: 2em;
    padding-top: 1.6em;
    border-top: 1px solid var(--border);
}

/* Images ----------------------------------------------------------------
   Centred logos/screenshots read fine (brief explicitly allows this); the
   operator logo specifically ("... logo" is the alt text on every single
   one in the migrated data) gets capped to a sane on-page size instead of
   printing at whatever the original upload's native dimensions were. */
.legacy-body img {
    display: block;
    margin: 0 auto 1.4em;
    border-radius: var(--radius-sm);
}
.legacy-body img[alt*="logo" i] {
    max-width: 220px;
    max-height: 70px;
    width: auto;
    object-fit: contain;
    border-radius: 0;
}
/* Old Facebook/Trustpilot embeds carry fixed pixel widths (one is
   width="340"); stop them forcing horizontal overflow on a narrow phone. */
.legacy-body iframe { max-width: 100%; }

/* Collapsed offer "tables" ------------------------------------------------
   The .offer--headline / .offer--terms class names are NOT invented for
   this pass — they survive verbatim in the migrated markup from the old
   theme's pricing-table shortcode (348 / 288 occurrences respectively) and
   give a real, stable hook to rebuild the visual grouping Divi used to
   provide: logo -> tinted headline block -> muted terms block, styled the
   same way the modern .bonus-box__headline / .bonus-box__terms already
   are, so a legacy offer entry and a live one carry the same visual
   language. */
.legacy-body .offer--headline {
    background: var(--brand-blue-light);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    padding: 16px 18px 6px;
    margin: 0;
}
/* Nesting depth and combination are both inconsistent in the source — a
   surviving comparison <table> (see below) has cells nesting
   .offer--headline 2-3 deep, .offer--terms inside .offer--headline, and
   .offer--headline inside .offer--terms, all for the one offer. Collapse
   every nested copy, in any combination, to a plain passthrough so only
   the OUTERMOST wrapper draws a box — otherwise a single offer nests 2-3
   tinted boxes inside each other. */
.legacy-body .offer--headline .offer--headline,
.legacy-body .offer--headline .offer--terms,
.legacy-body .offer--terms .offer--headline,
.legacy-body .offer--terms .offer--terms {
    display: contents;
}
.legacy-body .offer--headline :is(h2, h3, h4) {
    display: block;
    border-bottom: 0;
    padding-bottom: 0;
    margin: 0 0 .2em;
    color: var(--brand-blue-dark);
    font-size: 1.08rem;
}
.legacy-body .offer--headline p { margin: 0 0 .4em; }

/* Glued directly under the headline block as one continuous card — matches
   the offer -> terms adjacency the brief calls for. */
.legacy-body .offer--headline + .offer--terms,
.legacy-body .offer--headline + .main-txt {
    background: var(--bg-alt);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
    padding: 10px 18px 16px;
    margin: 0 0 1.6em;
}
.legacy-body .offer--terms,
.legacy-body .main-txt {
    font-size: .82rem;
    line-height: 1.55;
    color: var(--text-muted);
}
.legacy-body .offer--terms p,
.legacy-body .main-txt p { margin: 0 0 .5em; }
.legacy-body .offer--terms p:last-child,
.legacy-body .main-txt p:last-child { margin-bottom: 0; }
/* "Read Our X Review" / "VISIT SITE" — an internal link, not an outbound
   affiliate CTA, so it stays blue (green is reserved for /go/ buttons) but
   bold enough to read as the obvious next click in the block. */
.legacy-body .offer--terms a { font-weight: 700; }

/* Alignment normalisation -------------------------------------------------
   Divi centred nearly every paragraph and heading (1232 inline
   text-align:center occurrences in the deployed content) regardless of
   whether the content was a two-line offer headline or a five-sentence
   paragraph. Inline styles beat any external rule short of !important, so
   this has to fight them directly — but only for body copy: images and the
   offer-block text above are deliberately centred/boxed already and are
   excluded so this doesn't flatten them back to plain left-aligned text. */
.legacy-body p[style*="text-align: center"],
.legacy-body p[style*="text-align:center"],
.legacy-body :is(h1, h2, h3, h4, h5, h6)[style*="text-align: center"],
.legacy-body :is(h1, h2, h3, h4, h5, h6)[style*="text-align:center"] {
    text-align: left !important;
}
.legacy-body .offer--headline [style*="text-align"],
.legacy-body .offer--terms [style*="text-align"],
.legacy-body .main-txt [style*="text-align"] {
    text-align: left !important;
}

/* Cleanup ------------------------------------------------------------------
   The old editor left a lot of debris behind: empty heading/strong/em tags
   (some list items end in a spurious "<br> <strong></strong>" pair with
   nothing in it) and a defunct floating share-widget whose leftover <style>
   block applies SITE-WIDE once echoed into the page (see the note above the
   .at-about-fab rule). `:empty` deliberately excludes <span>/<a>/<li> here —
   several imported headings carry empty <span id="..."> anchor targets that
   still need to exist (and be scrollable-to) for old inbound links with a
   URL fragment. */
.legacy-body :is(h1, h2, h3, h4, h5, h6, p, strong, em):empty { display: none; }
.legacy-body br:has(+ strong:empty) { display: none; }

/* Defensive, unconditional, NOT scoped to .legacy-body: one migrated page
   still contains the actual widget markup (not just the <style> tag) for a
   defunct WP "AddToAny" floating share button, authored with
   position:fixed and z-index:999999 — several orders of magnitude above
   every z-index this design system uses (max 100, the skip link). Once
   that page's body is echoed into the DOM its <style> block is global, so
   it can float on top of the sticky header, the sticky mobile CTA bar, or
   any other page element regardless of where in the document it sits. */
.at-about-fab,
.at-about-fab__thumbnail,
.at-about-fab__meta {
    display: none !important;
}

/* Old inbound links/bookmarks to this content often carry a URL fragment
   pointing at an in-page id the old theme's table-of-contents generated
   (e.g. #Play_With_50_50_FREE_Spins — 58 such ids survive in the migrated
   content). Only #main-content had scroll-margin-top before this, so a
   visitor arriving via one of those old fragments landed with the target
   tucked directly under the sticky header — the same class of bug as the
   owner's "header floating over content" report, just triggered by
   navigation instead of a stray widget. Matches the header's own offset. */
.legacy-body :is(h1, h2, h3, h4, h5, h6, [id]) { scroll-margin-top: 84px; }

/* Real <table> elements (3 survive in the deployed content, with inline
   pixel widths per cell from the original theme) — border/padding already
   comes from .content-page__body th/td above; this adds the responsive
   behaviour those rules don't: a table wider than its column scrolls
   horizontally instead of squeezing text or breaking the page layout. */
.legacy-body table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
/* No real <th> survives in any of them — the first row is functionally a
   header (bold label cells) even though it was authored as plain <td>s. */
.legacy-body table tr:first-child td {
    background: var(--bg-alt);
    font-weight: 700;
    color: var(--text);
}

.not-found { padding: 72px 0; text-align: center; }
.not-found h1 { font-size: clamp(1.9rem, 4vw, 2.4rem); }
.not-found__cta { margin: 26px 0 4px; }
.not-found ul {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px 20px;
    margin: 26px 0;
    padding: 0;
}
.not-found ul a { font-weight: 700; }

/* Cards grid ------------------------------------------------------------- */
.listing-grid { padding: 36px 0 64px; }

/* 250px minimum produced four cramped columns at full width, which forced
   the offer — the single most click-driving element on a comparison card —
   down to a two-line whisper. 320px gives three roomy columns on desktop and
   two on tablet: fewer, bigger, faster to triage. */
.cards-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 20px;
}
@media (max-width: 700px) {
    .cards-grid { grid-template-columns: 1fr; gap: 16px; }
}

.site-card {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 20px 20px 18px;
    display: flex;
    flex-direction: column;
    background: #fff;
    box-shadow: var(--shadow-sm);
    position: relative;
    transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease;
}
.site-card:hover {
    box-shadow: var(--shadow-lg);
    transform: translateY(-3px);
    border-color: var(--border-strong);
}

/* Whole-card click target. The stretched pseudo-element belongs to the
   "Read review" anchor, so the card's large, easy-to-hit body routes to the
   review page while the outbound CTA — raised above it — keeps its own hit
   area. This is the cheapest CTR win available on a card grid: the average
   visitor aims at the logo or the offer text, not at the small ghost
   button. */
/* The stretch anchor must stay position:static — an `absolute` child resolves
   against the nearest POSITIONED ancestor, so giving the anchor itself
   `position: relative` would shrink the overlay to the size of the button
   instead of the card. Only .site-card carries position:relative. */
.site-card__stretch { position: static; }
.site-card__stretch::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    z-index: 1;
}
/* Raise ONLY the outbound button above the overlay, so it keeps its own hit
   area while the rest of the card routes to the review. */
.site-card__actions .btn--primary { position: relative; z-index: 2; }

/* Rank/status badge. Single slot, one badge at a time (see the precedence
   comment in partials/site-card.php) — two pills stacked at the same corner
   read as clutter and neither gets read. No longer green. */
.site-card__badge {
    position: absolute;
    top: -11px; left: 18px;
    background: var(--brand-blue);
    color: #fff;
    font-size: .68rem;
    font-weight: 800;
    letter-spacing: .04em;
    text-transform: uppercase;
    padding: 5px 12px;
    border-radius: 999px;
    box-shadow: var(--shadow-sm);
    z-index: 2;
}
.site-card__badge--top { background: var(--brand-blue-dark); }

/* The #1 card gets a heavier border rather than a different colour scheme:
   enough to create a focal point at the top of the grid, not so much that
   it looks like a paid slot. */
.site-card--featured { border: 2px solid var(--brand-blue); box-shadow: var(--shadow); }

/* Identity row. The logo used to be a full-width 64px grey slab — high
   visual weight, low decision value — and when a site had no logo the grey
   slab printed the name, which the <h3> underneath then printed again.
   Now: logo (when there is one) stacked over the name on the left, rating on
   the right, one line. The eye picks up "who" and "how good" in a single
   fixation and drops straight onto the offer. */
.site-card__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}
.site-card__id {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}
.site-card__logo {
    height: 38px;
    display: flex;
    align-items: center;
}
.site-card__logo img { max-height: 38px; width: auto; }
.site-card__name {
    margin: 0;
    font-size: 1.02rem;
    font-weight: 700;
    color: var(--text);
    overflow-wrap: anywhere;
}

.site-card__rating {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}
.site-card__rating .stars { font-size: .82rem; letter-spacing: 1px; }
.site-card__rating-value {
    font-weight: 800;
    color: #fff;
    background: var(--brand-blue-dark);
    padding: 3px 10px;
    border-radius: 999px;
    font-size: .82rem;
    white-space: nowrap;
}

/* THE OFFER. Promoted from a .96rem bold paragraph — which lost the size
   contest to the site name above it — to the largest text on the card, in a
   tinted panel that gives it its own block. On a comparison card the bonus
   is the reason the click happens; it should win the first fixation. */
.site-card__bonus {
    font-weight: 800;
    font-size: 1.12rem;
    line-height: 1.3;
    color: var(--brand-blue-dark);
    background: var(--brand-blue-light);
    border-radius: var(--radius-sm);
    padding: 12px 14px;
    margin: 0 0 10px;
}
.site-card__bonus--empty {
    background: var(--bg-alt);
    color: var(--text-muted);
    font-weight: 600;
    font-size: .92rem;
}

/* Licence / operator line — trust data pulled up next to the decision
   instead of living only on the review page. Real schema fields
   (bingo_sites.operator, bingo_sites.licence_no) or it doesn't render. */
.site-card__trust {
    font-size: .78rem;
    line-height: 1.45;
    color: var(--text-muted);
    margin: 0 0 8px;
    padding-left: 12px;
    border-left: 3px solid var(--brand-blue-light);
}

/* Terms stay adjacent to the offer and above the button — regulated
   vertical, this is not a candidate for demotion. Bumped from .74rem, which
   was below comfortable reading size, to .78rem at 5.9:1 contrast. */
.site-card__terms {
    font-size: .78rem;
    line-height: 1.45;
    color: var(--text-muted);
    margin: 0 0 14px;
}

.site-card__actions {
    margin-top: auto;
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}
.site-card__actions .btn {
    flex: 1 1 100%;
    padding: 11px 14px;
    font-size: .92rem;
    min-height: 46px;
}
/* Two-button state (an operator with a live affiliate link): the outbound
   CTA takes noticeably more width than the secondary so there is never a
   50/50 choice to make. Below 380px they stack instead of shrinking to
   unreadable. */
.site-card__no-action {
    margin: 0;
    font-size: .86rem;
    font-style: italic;
    color: var(--text-muted);
}
.site-card__actions--pair .btn { flex: 1 1 0; }
.site-card__actions--pair .btn--primary { flex-grow: 1.6; }
@media (max-width: 380px) {
    .site-card__actions--pair .btn { flex: 1 1 100%; }
}

/* Review layout ------------------------------------------------------- */
.review-layout { padding: 36px 0 64px; }

/* Rating + freshness on one line under the H1. Both are trust signals and
   both belong above the fold, next to the headline the visitor is deciding
   whether to believe. */
.review-header__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px 18px;
    margin-top: 10px;
}
.review-rating {
    display: inline-flex;
    align-items: center;
    gap: 10px;
}
.review-rating__value {
    font-weight: 800;
    background: var(--brand-blue-dark);
    color: #fff;
    padding: 4px 14px;
    border-radius: 999px;
    font-size: .9rem;
}
/* pages.updated_at — a real column, printed as-is. */
.review-updated { font-size: .84rem; color: var(--text-muted); }
.review-layout__grid {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 40px;
    margin-top: 24px;
    align-items: start;
}
.review-layout__body { max-width: 72ch; }
.review-layout__body h2 {
    margin-top: 1.5em;
    padding-bottom: .3em;
    border-bottom: 3px solid var(--brand-blue);
    display: inline-block;
}

.bonus-box {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: 24px;
    background: #fff;
    box-shadow: var(--shadow);
    position: sticky;
    top: 92px;
    overflow: hidden;
}
/* De-greened, like every other decorative gradient. */
.bonus-box::before {
    content: "";
    display: block;
    height: 4px;
    margin: -24px -24px 20px;
    background: linear-gradient(90deg, var(--brand-blue-dark), #4aa3e0);
}
.bonus-box__logo { max-height: 52px; width: auto; margin-bottom: 10px; }
.bonus-box__name { margin-bottom: 10px; font-size: 1.2rem; }
/* Matched to the card's offer treatment so the same information carries the
   same weight wherever the visitor meets it. */
.bonus-box__headline {
    font-weight: 800;
    color: var(--brand-blue-dark);
    background: var(--brand-blue-light);
    padding: 12px 14px;
    border-radius: var(--radius-sm);
    font-size: 1.1rem;
    line-height: 1.3;
    margin: 0 0 14px;
}

/* Terms sit directly beneath the CTA rather than at the bottom of the box
   under the facts table. This is both the more compliant placement (terms
   adjacent to the offer at the point of decision, not buried below fold-fold
   detail) and the more converting layout — offer, action, then the
   supporting detail for anyone still deciding. */
.bonus-box__terms {
    font-size: .76rem;
    line-height: 1.5;
    color: var(--text-muted);
    margin: 12px 0 0;
}
.bonus-box__facts {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px 14px;
    font-size: .82rem;
    margin: 18px 0 0;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}
.bonus-box__facts dt { color: var(--text-muted); font-weight: 600; margin-bottom: 2px; }
.bonus-box__facts dd { margin: 0; font-weight: 600; }
.bonus-box__pending {
    font-style: italic;
    color: var(--text-muted);
    font-size: .88rem;
    margin: 0;
}
.bonus-box__compare-link { display: block; text-align: center; margin-top: 16px; font-size: .85rem; }

@media (max-width: 860px) {
    /* Flex rather than a one-column grid purely so `order` is available:
       the offer box has to come BEFORE the review body on mobile. Left as a
       stacked grid it landed after the entire article — a visitor on a phone
       reached the CTA only by scrolling past every word of the review, which
       on a migrated 1,200-word page is most of them not reaching it at all.
       Done with `order` and not by moving the markup: the DOM keeps the
       review body first, which is the right reading/tab order and the right
       thing for Google to see first. The sidebar is a complementary <aside>,
       so the visual/DOM divergence is the acceptable kind — no interactive
       control changes meaning based on where it appears. */
    .review-layout__grid {
        display: flex;
        flex-direction: column;
        gap: 28px;
    }
    .review-layout__sidebar { order: -1; }
    .bonus-box { position: static; }
}

/* Sticky mobile offer bar -------------------------------------------------
   Renders on review pages ONLY when bingo_sites.affiliate_url is set (see
   review.php) — today that is nowhere, so this is dormant machinery waiting
   for programme approval. When it lights up it is the single biggest mobile
   CTR lever on the site: a review page is a long scroll, and without this
   the CTA exists only in a box the visitor has already scrolled past.

   Deliberately NOT a modal, interstitial or timed pop-up: a persistent bar
   the visitor can ignore is not a dark pattern; something that interrupts
   them is. The offer terms ride along inside the bar so the compliance line
   travels with the button rather than being left behind up the page.
   ------------------------------------------------------------------------ */
.sticky-cta {
    display: none;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 40;
    background: #fff;
    border-top: 1px solid var(--border);
    box-shadow: 0 -6px 22px rgba(15, 23, 42, .16);
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
}
.sticky-cta__inner {
    display: flex;
    align-items: center;
    gap: 12px;
    max-width: var(--max-width);
    margin: 0 auto;
}
.sticky-cta__copy { min-width: 0; flex: 1 1 auto; }
.sticky-cta__offer {
    display: block;
    font-weight: 800;
    font-size: .92rem;
    line-height: 1.25;
    color: var(--brand-blue-dark);
    /* Two lines maximum — the bar must never grow tall enough to eat the
       content it sits over. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.sticky-cta__terms {
    display: block;
    font-size: .68rem;
    line-height: 1.35;
    color: var(--text-muted);
    margin-top: 2px;
}
.sticky-cta .btn { flex: 0 0 auto; padding: 12px 20px; font-size: .95rem; }

@media (max-width: 860px) {
    .sticky-cta { display: block; }
    /* Reserve the bar's height so the footer's compliance text and the last
       paragraph of a review are never permanently hidden underneath it. */
    body.has-sticky-cta { padding-bottom: 88px; }
}

/* Footer ------------------------------------------------------------------ */
.site-footer {
    background: var(--brand-dark);
    color: #c7ccd3;
    padding: 48px 0 32px;
    margin-top: 56px;
    font-size: .85rem;
}
.site-footer a { color: #9fd1f2; }
.site-footer a:hover { color: #fff; }
.site-footer__compliance {
    display: flex;
    align-items: flex-start;
    gap: 16px;
    flex-wrap: wrap;
    padding-bottom: 28px;
    margin-bottom: 24px;
    border-bottom: 1px solid rgba(255,255,255,.14);
}
.site-footer__compliance p { margin: 0 0 10px; line-height: 1.6; }
.site-footer__compliance p:last-child { margin-bottom: 0; }
.site-footer__age-badge {
    flex-shrink: 0;
    border-radius: 6px;
    box-shadow: 0 0 0 1px rgba(255,255,255,.18);
}
.site-footer__links ul {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 8px 22px;
    padding: 0;
    margin: 0;
}
.site-footer__links a { font-weight: 600; }
