/* ============================================================
   LoreLoom — landing
   Product brand: purple/lilac on deep dark, Fraunces + Inter.
   Families are defined in tokens.css, not here.
   Layout reworked to feel alive & hand-made (no fake reviews).
   ============================================================ */

:root {
    --bg-dark: #0c0a12;
    /* deep purple-black, matches product */
    --bg-2: #15121e;
    --panel: #191527;
    --panel-2: #201a30;
    --line: rgba(226, 179, 255, 0.10);
    --line-strong: rgba(226, 179, 255, 0.20);

    --text-primary: #f4f0fa;
    --text-secondary: #b3a9c9;
    --text-faint: #877e9c;

    --accent-color: var(--brand-accent, #a777e3);
    --accent-color-2: var(--brand-accent-2, #6e8efb);
    --accent-soft: #e2b3ff;
    --gold: #ffd700;
    --accent-gradient: var(--brand-gradient, linear-gradient(135deg, #6e8efb, #a777e3));

    /* --font-display / --font-body / --font-heading now live in tokens.css,
       which every page links first. They were defined here, which left the three
       pages that load styles.css instead of landing.css with no family tokens at
       all. Do not redefine them here — a second definition is how the two
       drifted apart in the first place. */

    --radius: var(--radius-md, 18px);
    --shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.65);
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    scroll-behavior: smooth;
}

::selection {
    background: var(--accent-color);
    color: #fff;
}

::-webkit-scrollbar {
    width: 10px;
}

::-webkit-scrollbar-track {
    background: var(--bg-dark);
}

::-webkit-scrollbar-thumb {
    background: linear-gradient(var(--accent-color), var(--accent-color-2));
    border-radius: 10px;
    border: 3px solid var(--bg-dark);
}

body.landing-body {
    background-color: var(--bg-dark);
    color: var(--text-primary);
    font-family: var(--font-body);
    line-height: 1.65;
    overflow-x: hidden;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* Kill the grey rectangle Android WebView flashes over every tapped element.
       styles.css sets this on `body` for the app pages, but styles.css is only
       linked on create_character / dashboard / discover — landing.css is what
       reaches the other 15, so the declaration has to exist in both places. Keep
       the two in step. */
    -webkit-tap-highlight-color: transparent;
    /* soft brand glows + faint grain so it doesn't feel flat/templated */
    background-image:
        radial-gradient(1200px 600px at 80% -10%, rgba(167, 119, 227, 0.14), transparent 60%),
        radial-gradient(900px 500px at -10% 20%, rgba(110, 142, 251, 0.10), transparent 55%),
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
    background-attachment: fixed, fixed, fixed;
}

h1,
h2,
h3 {
    font-family: var(--font-display);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1.08;
}

a {
    text-decoration: none;
    color: inherit;
}

.wrap {
    max-width: 1160px;
    margin: 0 auto;
    padding: 0 28px;
}

.accent-dot {
    color: var(--accent-color);
}

.gradient-text {
    background: var(--accent-gradient);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

/* ---------- Navbar ---------- */
.navbar {
    position: fixed;
    /* Offset from the status bar, not from the window: the app draws edge-to-edge,
       so a bare `top: 16px` puts this floating pill directly against the clock and
       signal icons. --safe-bottom's sibling is 0 in a browser, so the web layout is
       byte-identical to before. The mobile override below needs the same treatment. */
    top: calc(16px + var(--safe-top));
    left: 50%;
    /* The horizontal insets are the landscape half of the same problem. Rotate a
       cutout phone and --safe-left/--safe-right stop being 0 -- 48.76px on the left
       on a Pixel 6, where the hole-punch rotates to the short edge. The navbar is
       the only chrome on the page anchored within 48px of the screen edge (every
       other wrapper uses a percentage gutter that already clears it), so it is the
       only rule that needs to know.

       Subtracting both insets from the width keeps the bar inside the safe box, and
       the translate offset re-centres it in that box rather than in the window --
       without it, trimming an asymmetric pair of insets from a centred element
       leaves the bar off-centre by half their difference. Both tokens are 0 in a
       browser, so the whole expression collapses to translateX(-50%) and
       calc(100% - 48px): the live site is unchanged. */
    transform: translateX(calc(-50% + (var(--safe-left) - var(--safe-right)) / 2));
    width: calc(100% - 48px - var(--safe-left) - var(--safe-right));
    max-width: 1200px;
    z-index: 100;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 14px 12px 24px;
    border-radius: 999px;
    background: rgba(21, 18, 30, 0.55);
    border: 1px solid var(--line);
    backdrop-filter: blur(16px) saturate(140%);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.6),
        inset 0 1px 0 rgba(255, 255, 255, 0.04);
    transition: padding .3s ease, background .3s ease, border-color .3s ease, box-shadow .3s ease;
}

.navbar.scrolled {
    padding: 9px 12px 9px 22px;
    background: rgba(15, 13, 22, 0.82);
    border-color: var(--line-strong);
    box-shadow: 0 14px 34px -14px rgba(0, 0, 0, 0.75),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
}

.logo-text {
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 800;
    letter-spacing: -0.03em;
}

.nav-links {
    display: none;
    gap: 6px;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
}

@media (min-width: 860px) {
    .nav-links {
        display: flex;
    }
}

/* The link row is absolutely centred on the navbar (base rule above), which means
   it cannot know whether it fits: it turns on at 860px and centres itself
   regardless of what is already occupying that space. The two sides are not
   symmetric -- the logo is 107px and .nav-actions is 250px -- so centring a 407px
   row on the bar pushes its right edge into the buttons long before the bar
   actually runs out of room.

   Measured on device across four widths, links/actions gap by viewport:
   914px -35   983px -1   1000px +8   1021px +18
   So "How it Works" overlapped "Log in" from 860px (where the row appears) all the
   way to 983px -- a 123px band that includes phone landscape, 914px on a Pixel
   6, which is what surfaced it. It was a live-site bug at those widths too.

   Inside the band the row goes in-flow, so the navbar's own
   `justify-content: space-between` places it in the space that is actually free
   rather than on the bar's centre line. That is the whole fix: the three groups
   have always fit -- only the centring was wrong. Above 1020px the absolute
   centring is kept and every computed value is byte-identical to before, because
   that is the width where exact centring stops colliding on its own.

   Re-measured 2026-08-30, after the display face became Fraunces. The wordmark is
   the widest thing the face touches up here, and it grew 107px -> 117.1px, which
   came straight out of the slack:

       viewport   logo + links + actions   content box   slack   visible ink gap
        860px       117.1+405.7+249.2=772      774        2.0px       62px
        914px       117.1+407.4+250.3=774.8    828       53.2px       41px
        983px                    "  774.8      897      122.2px       75px
       1020px                    "  774.8      934      159.2px       94px

   860px is the worst case, not 914px -- the band's lower bound is where the row
   turns on, and below it the links go back to the mobile nav. 2px of slack sounds
   alarming and is not: `boxGap` there is 0px, but .nav-links a carries horizontal
   padding, so the gap between the "How it Works" INK and the "Log in" pill is
   62px, and .nav-links itself absorbs the last of it by compressing 407.4 -> 405.7.
   Nothing overlaps at any width in the band.

   What the 2px does mean is that this row has no headroom left. Adding a fifth
   nav item, a longer label, or a wider fallback face will collide at 860px, and
   the 8px that used to absorb that is now inside the wordmark. Re-run
   llshots/navink.mjs, which measures ink rather than boxes, before changing
   anything in this bar. */
@media (min-width: 860px) and (max-width: 1020px) {
    .nav-links {
        position: static;
        left: auto;
        transform: none;
    }
}

.nav-links a {
    color: var(--text-secondary);
    font-size: 0.92rem;
    font-weight: 500;
    padding: 7px 15px;
    border-radius: 999px;
    transition: color .2s ease, background .2s ease;
}

.nav-links a:hover {
    color: var(--text-primary);
    background: rgba(226, 179, 255, 0.08);
}

.nav-actions {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* ---------- Buttons ---------- */
.primary-btn {
    font-family: var(--font-body);
    background: linear-gradient(135deg, #6e8efb, #a777e3, #6e8efb);
    background-size: 200% auto;
    /* Both stops here are the brand pair, so this is the same white-on-gradient
       failure tokens.css documents: measured 3.03:1 at 12.16px/650, against the
       4.5:1 it needs at that size. It was missed because the contrast pass only
       edited styles.css, and this button lives in landing.css — yet it is the
       site's most-shown CTA, sitting in the nav of every marketing and legal
       page. Same resolution as everywhere else: keep the fill, darken the label.
       #1a1523 measures 5.89:1 on #6e8efb and 5.44:1 on #a777e3.
       This single declaration also covers `.primary-btn i` and every :hover,
       :active, .sm, .lg and media-query variant — none of them re-declare
       colour, so there is exactly one place to change. */
    color: var(--on-brand-gradient);
    border: none;
    border-radius: 999px;
    font-weight: 650;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    display: inline-flex;
    align-items: center;
    gap: 9px;
    transition: transform .2s cubic-bezier(.2, .8, .2, 1), box-shadow .25s ease, background-position .5s ease;
    box-shadow: 0 6px 20px -6px rgba(167, 119, 227, 0.6),
        inset 0 1px 0 rgba(255, 255, 255, 0.28);
}

/* moving sheen so the fill doesn't read as flat */
.primary-btn::before {
    content: '';
    position: absolute;
    top: 0;
    left: -60%;
    width: 45%;
    height: 100%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.35), transparent);
    transform: skewX(-18deg);
    transition: left .6s ease;
}

.primary-btn:hover::before {
    left: 130%;
}

.primary-btn:hover {
    background-position: right center;
    transform: translateY(-2px);
    box-shadow: 0 14px 30px -8px rgba(167, 119, 227, 0.7),
        inset 0 1px 0 rgba(255, 255, 255, 0.28);
}

.primary-btn:active {
    transform: translateY(0);
}

.primary-btn.sm {
    padding: 9px 18px;
    font-size: 0.9rem;
}

.primary-btn.lg {
    padding: 15px 30px;
    font-size: 1.02rem;
}

.primary-btn i {
    transition: transform .25s ease;
}

/* responsive button label: full text by default, short text on narrow phones */
.label-short {
    display: none;
}

.primary-btn:hover i {
    transform: translateX(3px);
}

.login-btn {
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid var(--line-strong);
    color: var(--text-primary);
    padding: 9px 20px;
    border-radius: 999px;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.9rem;
    cursor: pointer;
    transition: background .25s ease, border-color .25s ease, box-shadow .25s ease;
}

.login-btn:hover {
    background: rgba(255, 255, 255, 0.09);
    border-color: rgba(167, 119, 227, 0.5);
    box-shadow: 0 0 0 3px rgba(167, 119, 227, 0.1);
}

.secondary-btn {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-primary);
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    padding: 15px 28px;
    font-family: var(--font-body);
    font-size: 1.02rem;
    font-weight: 600;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    transition: background .25s ease, border-color .25s ease, transform .2s ease;
}

.secondary-btn:hover {
    background: rgba(255, 255, 255, 0.08);
    border-color: rgba(167, 119, 227, 0.5);
    transform: translateY(-2px);
}

/* ---------- Hero ---------- */
.hero {
    position: relative;
    padding: 160px 0 90px;
    /* Clears the fixed navbar above, which is itself offset by --safe-top
       (see the comment on .navbar). This padding has to track the same inset
       or the gap between the two closes as the status bar grows: on a phone
       reporting 48.76px the designed 66px of breathing room measured 17px.
       --safe-top is 0 in a browser, so the live site is unchanged. The mobile
       override below needs the same treatment; pages.css does this for the
       four wrappers that clear the navbar on the other 14 pages. */
    padding-top: calc(160px + var(--safe-top));
}

.hero-inner {
    display: grid;
    grid-template-columns: 1.05fr 0.95fr;
    gap: 56px;
    align-items: center;
}

@media (max-width: 940px) {
    .hero-inner {
        grid-template-columns: 1fr;
        gap: 48px;
    }
}

/* ---------- Hero rotating showcase ---------- */
.hero-slides {
    display: grid;
    /* inactive slides are translated ±46px and still take grid space; clip that
       horizontally so the page never gains a sideways scroll (which on mobile
       widens the layout viewport and pushes the fixed navbar off-screen) */
    overflow-x: clip;
}

/* Both slides live in the same grid cell (`grid-area: 1/1`), so whatever opacity they
   hold at the same instant is composited on top of each other. Until 2026-08-21 both
   fades ran .6s starting together, and `visibility 0s linear .6s` deliberately kept the
   outgoing slide visible for the whole of it -- so for 600ms two different headlines
   were superimposed, measured passing through 0.53/0.47 opacity simultaneously, giving
   ~293ms of genuinely unreadable text every 7s on the first screen every visitor sees.

   The fix is to stagger the two fades so their readable windows never intersect: the
   outgoing slide fades out in .25s, and the incoming one waits .25s before starting its
   .35s fade in. Same .6s total, same rhythm, but at any instant only one slide has a
   readable opacity.

   The .7s transform is deliberately NOT staggered, for two reasons: the slide motion
   overlapping is the intended effect and is invisible during the first .25s anyway, and
   index.html:569 clears `.is-leaving` on a 700ms timer keyed to exactly this duration.
   Changing it here would desynchronise the two. */
.hero-slide {
    grid-area: 1 / 1;
    /* all slides stack in the same cell → container = tallest slide */
    opacity: 0;
    visibility: hidden;
    transform: translateX(46px);
    /* visibility flips at .25s, the moment opacity reaches 0 -- any later and the
       invisible slide keeps hit-testing, any earlier and the fade-out is cut off */
    transition: opacity .25s ease, transform .7s cubic-bezier(.2, .8, .2, 1),
        visibility 0s linear .25s;
    pointer-events: none;
}

.hero-slide.is-active {
    opacity: 1;
    visibility: visible;
    transform: none;
    pointer-events: auto;
    transition: opacity .35s ease .25s, transform .7s cubic-bezier(.2, .8, .2, 1);
}

/* outgoing slide leaves to the left */
.hero-slide.is-leaving {
    transform: translateX(-46px);
}

/* Only the visible slide animates its chat bubbles (replays on each switch) */
.hero-slide:not(.is-active) .msg {
    animation: none;
    opacity: 0;
}

/* The hero rotates itself every 7s and slides 46px doing it, which is exactly the kind
   of unrequested movement prefers-reduced-motion exists for. Drop the travel and cut the
   crossfade to a bare minimum rather than freezing the rotation, which would leave the
   second slide's content permanently unreachable for anyone who set the preference. */
@media (prefers-reduced-motion: reduce) {

    .hero-slide {
        transform: none;
        transition: opacity .12s linear, visibility 0s linear .12s;
    }

    .hero-slide.is-active {
        transform: none;
        transition: opacity .12s linear .12s;
    }

    .hero-slide.is-leaving {
        transform: none;
    }
}

/* Slide dot controls */
.hero-dots {
    display: flex;
    justify-content: center;
    gap: 12px;
    margin-top: 48px;
    position: relative;
    z-index: 2;
}

.hero-dot {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px 8px 12px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: rgba(255, 255, 255, 0.03);
    color: var(--text-secondary);
    font-family: var(--font-body);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    transition: color .25s ease, border-color .25s ease, background .25s ease;
}

.hero-dot span {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--text-faint);
    transition: background .25s ease, transform .25s ease;
}

.hero-dot:hover {
    color: var(--text-primary);
    border-color: rgba(167, 119, 227, 0.4);
}

.hero-dot.is-active {
    color: var(--text-primary);
    border-color: rgba(167, 119, 227, 0.55);
    background: rgba(167, 119, 227, 0.1);
}

.hero-dot.is-active span {
    background: var(--accent-color);
    transform: scale(1.15);
    box-shadow: 0 0 10px rgba(167, 119, 227, 0.7);
}

.eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent-soft);
    margin-bottom: 22px;
}

.eyebrow .dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--accent-color);
    box-shadow: 0 0 0 0 rgba(167, 119, 227, 0.5);
    animation: livePulse 2.4s infinite;
}

@keyframes livePulse {
    0% {
        box-shadow: 0 0 0 0 rgba(167, 119, 227, 0.5);
    }

    70% {
        box-shadow: 0 0 0 8px rgba(167, 119, 227, 0);
    }

    100% {
        box-shadow: 0 0 0 0 rgba(167, 119, 227, 0);
    }
}

.hero-title {
    font-size: clamp(2.7rem, 5.4vw, 4.4rem);
    font-weight: 800;
    margin-bottom: 22px;
    /* Both hero headlines used to carry a hard-coded <br> at the break the
       author liked at 1440px. A manual break in a clamp()ed headline is only
       correct at the one width it was eyeballed at: the font here ranges from
       2.1rem to 4.4rem, so the same break gives a balanced two-line block on a
       desktop and a two-word orphan on a 914px tablet. `balance` recomputes it
       per width instead. It only applies to blocks of a few lines, which is
       exactly the case here, and degrades to normal wrapping where unsupported.
       Same reasoning for .section-title below — keep the two in step. */
    text-wrap: balance;
}

.hero-subtitle {
    font-size: 1.13rem;
    color: var(--text-secondary);
    max-width: 480px;
    margin-bottom: 34px;
}

.cta-group {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    align-items: center;
}

.hero-note {
    margin-top: 20px;
    font-size: 0.85rem;
    color: var(--text-faint);
    display: flex;
    align-items: center;
    gap: 8px;
}

.hero-note i {
    color: var(--accent-color-2);
}

/* ---------- Hero: live product demo (chat) ---------- */
.demo {
    position: relative;
}

.demo-glow {
    position: absolute;
    inset: -40px;
    background: radial-gradient(closest-side, rgba(167, 119, 227, 0.22), transparent 75%);
    filter: blur(20px);
    z-index: 0;
}

.chat-window {
    position: relative;
    z-index: 1;
    background: linear-gradient(180deg, var(--panel), var(--bg-2));
    border: 1px solid var(--line-strong);
    border-radius: 22px;
    box-shadow: var(--shadow);
    overflow: hidden;
    transform: rotate(-1deg);
    transition: transform .5s cubic-bezier(.2, .8, .2, 1);
}

.chat-window:hover {
    transform: rotate(0deg);
}

.chat-top {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px 18px;
    border-bottom: 1px solid var(--line);
    background: rgba(0, 0, 0, 0.2);
}

.chat-ava {
    width: 42px;
    height: 42px;
    border-radius: 50%;
    object-fit: cover;
    border: 2px solid rgba(167, 119, 227, 0.6);
}

.chat-who strong {
    font-family: var(--font-display);
    font-size: 1.02rem;
    font-weight: 700;
    display: block;
    line-height: 1.2;
}

.chat-who span {
    font-size: 0.78rem;
    color: #6fcf7f;
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.chat-who span::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #6fcf7f;
}

.chat-top .fa-ellipsis-vertical {
    margin-left: auto;
    color: var(--text-faint);
}

.chat-body {
    padding: 22px 18px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-height: 320px;
    background: radial-gradient(500px 200px at 100% 0%, rgba(110, 142, 251, 0.08), transparent);
}

.msg {
    max-width: 82%;
    padding: 11px 15px;
    font-size: 0.94rem;
    line-height: 1.5;
    border-radius: 16px;
    opacity: 0;
    transform: translateY(8px);
    animation: msgIn .5s forwards;
}

.msg.them {
    align-self: flex-start;
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-bottom-left-radius: 5px;
}

.msg.them em {
    color: var(--accent-soft);
    font-style: italic;
}

.msg.me {
    align-self: flex-end;
    background: rgba(167, 119, 227, 0.16);
    border: 1px solid rgba(167, 119, 227, 0.35);
    color: var(--text-primary);
    border-bottom-right-radius: 5px;
}

.msg.m1 {
    animation-delay: .3s;
}

.msg.m2 {
    animation-delay: 1.2s;
}

.msg.m3 {
    animation-delay: 2.1s;
}

.msg.typing {
    align-self: flex-start;
    display: flex;
    gap: 5px;
    padding: 14px 16px;
    background: var(--panel-2);
    border: 1px solid var(--line);
    border-bottom-left-radius: 5px;
    animation-delay: 3.0s;
}

.msg.typing span {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--text-faint);
    animation: typedot 1.3s infinite;
}

.msg.typing span:nth-child(2) {
    animation-delay: .2s;
}

.msg.typing span:nth-child(3) {
    animation-delay: .4s;
}

@keyframes typedot {

    0%,
    100% {
        opacity: .35;
        transform: translateY(0);
    }

    50% {
        opacity: 1;
        transform: translateY(-4px);
    }
}

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

.chat-input {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    border-top: 1px solid var(--line);
    background: rgba(0, 0, 0, 0.25);
}

.chat-input .fake-field {
    flex: 1;
    color: var(--text-faint);
    font-size: 0.9rem;
    padding: 9px 14px;
    background: var(--bg-dark);
    border: 1px solid var(--line);
    border-radius: 999px;
}

.chat-input .send {
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: 50%;
    background: var(--accent-gradient);
    color: #fff;
    display: grid;
    place-items: center;
    font-size: 0.9rem;
}

/* little floating tags on the demo */
.demo-tag {
    position: absolute;
    z-index: 2;
    background: var(--panel);
    border: 1px solid var(--line-strong);
    border-radius: 12px;
    padding: 9px 13px;
    font-size: 0.78rem;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 8px;
    box-shadow: var(--shadow);
    animation: floaty 5s ease-in-out infinite alternate;
}

.demo-tag i {
    color: var(--accent-color);
}

.demo-tag.t1 {
    top: -18px;
    right: 14px;
}

/* t2 hangs BELOW the card, clear of it. It used to sit at `bottom: 40px`,
   which put it inside the card over the composer row — measured at
   768/914/983/1000/1021/1440px it overlapped .chat-input by ~130x22-26px and
   covered 116px of the "Write your reply…" placeholder at every one of them.
   The pill also carries z-index: 2, so it won the paint and the mockup showed a
   chat box with its own prompt text half-hidden behind a floating badge.

   It cannot mirror t1's -18px, which is the obvious-looking fix and measures
   WORSE (37px of .chat-input covered instead of 25px). Three measured reasons:

     - .chat-window is rotated ~1deg (transform: matrix(0.999848, -0.0174524,
       ...)), so its VISUAL bottom sits 4px below .demo's layout bottom at
       1440/983 and 6px below at 768. `bottom` here resolves against .demo —
       t2's offsetParent — so offset 0 is already inside the card.
     - .chat-input is position: static inside that rotated box and reaches the
       card's bottom edge, so anything straddling that edge lands on the
       composer. There is no horizontal escape: the field spans the full row.
     - floaty translates -12px, i.e. UPWARD, back toward the card. The drift
       has to be added to the clearance, not subtracted from it.

   64px = 7 (worst-case rotation overhang, at 860-914px) + 41 (pill height) +
   12 (drift) + 4 spare. Measured at 561/600/768/860/914/983/1000/1021/1200/
   1440/1920: at the top of the float the pill's top edge clears the card by
   4-8px, and it never touches .chat-input, .fake-field or .chat-body at any
   point in the cycle. At rest it hangs 23px below the card. .demo-glow is
   inset: -40px so the pill stays inside the halo for most of the cycle, and the
   tightest width still leaves 108px of empty room before .intro-section, so
   nothing below is disturbed.

   Only paints at >=561px — the max-width: 560px block near the end of this file
   drops t2 entirely and lays t1 out statically. */
.demo-tag.t2 {
    bottom: -64px;
    left: -26px;
    animation-delay: -2s;
}

@keyframes floaty {
    to {
        transform: translateY(-12px);
    }
}

@media (max-width: 520px) {
    .demo-tag.t2 {
        left: -6px;
    }
}

/* ---------- Section scaffolding ---------- */
section {
    position: relative;
}

.section-head {
    max-width: 620px;
    margin-bottom: 50px;
}

.section-head.center {
    margin-left: auto;
    margin-right: auto;
    text-align: center;
}

.section-kicker {
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--accent-color);
    margin-bottom: 14px;
}

.section-title {
    font-size: clamp(2rem, 3.6vw, 2.9rem);
    font-weight: 800;
    margin-bottom: 14px;
    text-wrap: balance;   /* see .hero-title */
}

.section-subtitle {
    color: var(--text-secondary);
    font-size: 1.08rem;
}

/* ---------- Intro prose ---------- */
.intro-section {
    padding: 90px 0;
    border-top: 1px solid var(--line);
}

.intro-inner {
    max-width: 760px;
    margin: 0 auto;
    text-align: center;
}

.intro-inner .section-title {
    margin-bottom: 22px;
}

.intro-inner p {
    color: var(--text-secondary);
    font-size: 1.1rem;
    line-height: 1.85;
    margin-bottom: 18px;
}

.intro-inner a {
    color: var(--accent-soft);
    border-bottom: 1px solid rgba(226, 179, 255, 0.35);
}

.intro-inner a:hover {
    color: var(--accent-color);
}

/* ---------- Gallery (real product cards) ---------- */
.gallery-section {
    padding: 90px 0;
}

.gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 20px;
}

.char-card {
    position: relative;
    border-radius: var(--radius);
    overflow: hidden;
    border: 1px solid var(--line);
    aspect-ratio: 3 / 4;
    background-size: cover;
    background-position: center;
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), border-color .35s ease, box-shadow .35s ease;
    cursor: pointer;
    isolation: isolate;
}

.char-card::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, transparent 30%, rgba(12, 10, 18, 0.55) 62%, rgba(12, 10, 18, 0.96) 100%);
    z-index: 1;
    transition: background .35s ease;
}

.char-card:hover {
    transform: translateY(-6px);
    border-color: rgba(167, 119, 227, 0.5);
    box-shadow: 0 24px 40px -18px rgba(0, 0, 0, 0.7);
}

.char-card-body {
    position: absolute;
    inset: auto 0 0 0;
    z-index: 2;
    padding: 18px 16px;
}

.char-tag {
    display: inline-block;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent-soft);
    background: rgba(167, 119, 227, 0.16);
    border: 1px solid rgba(167, 119, 227, 0.35);
    padding: 3px 9px;
    border-radius: 999px;
    margin-bottom: 10px;
}

.char-card-body h3 {
    font-size: 1.25rem;
    margin-bottom: 5px;
}

.char-card-body p {
    font-size: 0.86rem;
    color: var(--text-secondary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.char-meta {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-top: 12px;
    font-size: 0.78rem;
    color: var(--text-faint);
}

.char-meta span {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.char-meta i {
    color: var(--accent-color);
}

/* live "online" badge, top-right of a card */
.char-online {
    position: absolute;
    z-index: 3;
    top: 12px;
    right: 12px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #c9f7d3;
    background: rgba(20, 40, 26, 0.6);
    border: 1px solid rgba(111, 207, 127, 0.35);
    padding: 4px 9px;
    border-radius: 999px;
    backdrop-filter: blur(6px);
}

.char-online::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #6fcf7f;
    box-shadow: 0 0 6px #6fcf7f;
}

/* hover "Start chatting" ribbon that slides up */
.char-hover {
    position: absolute;
    z-index: 3;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 12px;
    text-align: center;
    font-size: 0.88rem;
    font-weight: 650;
    color: #fff;
    background: var(--accent-gradient);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transform: translateY(100%);
    transition: transform .3s cubic-bezier(.2, .8, .2, 1);
}

.char-card:hover .char-hover {
    transform: translateY(0);
}

/* locked teaser card that lures sign-up */
.char-card.locked::before {
    background: linear-gradient(180deg, rgba(12, 10, 18, 0.55), rgba(12, 10, 18, 0.9));
    backdrop-filter: blur(3px);
}

.char-lock-body {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 10px;
    padding: 22px;
}

.lock-badge {
    width: 46px;
    height: 46px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: rgba(167, 119, 227, 0.18);
    border: 1px solid rgba(167, 119, 227, 0.4);
    color: var(--accent-soft);
    font-size: 1.1rem;
    margin-bottom: 4px;
}

.char-lock-body strong {
    font-family: var(--font-display);
    font-size: 1.15rem;
    font-weight: 700;
}

.char-lock-body span {
    font-size: 0.85rem;
    color: var(--text-secondary);
    line-height: 1.5;
    max-width: 200px;
}

.char-lock-body .primary-btn {
    margin-top: 8px;
}

/* The two fixed-size items in a flex column that cannot absorb overflow: a badge
   is a circle or it is an ellipse, and a 36px button is a tap target. If the copy
   above them ever wraps to one more line than the card has room for — a longer
   translation, a user-set larger font — the shrink has to land on the paragraph,
   which merely clips. Without this the badge and the CTA went first, which is
   exactly what happened on the full-row mobile variant (see the media query at
   the 560px breakpoint for the measurements). */
.lock-badge,
.char-lock-body .primary-btn {
    flex-shrink: 0;
}

.gallery-cta {
    text-align: center;
    margin-top: 44px;
}

/* ---------- Diary chat demo (hero slide 2) ---------- */
/* mirror the hero demo but rotate the other way */
.diary-demo .chat-window {
    transform: rotate(1deg);
}

.diary-demo .chat-window:hover {
    transform: rotate(0deg);
}

.diary-ava {
    width: 42px;
    height: 42px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    color: #fff;
    font-size: 1rem;
    background: linear-gradient(135deg, var(--accent-color), var(--accent-color-2));
    border: 2px solid rgba(167, 119, 227, 0.6);
}

.diary-demo .chat-who span {
    color: var(--accent-soft);
}

.diary-demo .chat-who span::before {
    background: var(--accent-soft);
}

.diary-face {
    color: var(--text-faint);
    font-size: 1.05rem;
    display: flex;
    align-items: center;
}

/* Diary demo message timing (4 lines instead of the hero's 3) */
.diary-demo .msg.m4 {
    animation-delay: 3.0s;
}

.diary-demo .msg.typing {
    animation-delay: 3.9s;
}

/* ---------- Process (thread timeline) ---------- */
.process-section {
    padding: 90px 0;
    border-top: 1px solid var(--line);
}

.thread {
    position: relative;
    max-width: 720px;
    margin: 0 auto;
    padding-left: 12px;
}

/* the vertical "thread" running through the nodes */
.thread-line {
    position: absolute;
    left: 39px;
    top: 20px;
    bottom: 40px;
    width: 2px;
    background: linear-gradient(180deg,
            rgba(110, 142, 251, 0.5),
            rgba(167, 119, 227, 0.5) 60%,
            var(--accent-soft));
    border-radius: 2px;
}

.thread-step {
    position: relative;
    display: flex;
    gap: 26px;
    align-items: stretch;
    padding-bottom: 22px;
}

.thread-step:last-child {
    padding-bottom: 0;
}

.thread-node {
    position: relative;
    z-index: 2;
    flex: none;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    font-size: 1.15rem;
    color: var(--accent-soft);
    background: var(--panel-2);
    border: 1px solid var(--line-strong);
    box-shadow: 0 0 0 6px var(--bg-dark);
    transition: transform .3s ease, color .3s ease, border-color .3s ease;
}

.thread-step:hover .thread-node {
    transform: scale(1.08);
    border-color: rgba(167, 119, 227, 0.6);
}

.thread-node.accent {
    color: #fff;
    background: var(--accent-gradient);
    border-color: transparent;
    box-shadow: 0 0 0 6px var(--bg-dark), 0 0 26px -2px rgba(167, 119, 227, 0.7);
}

.thread-card {
    flex: 1;
    background: linear-gradient(180deg, var(--panel), var(--bg-2));
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 22px 26px;
    transition: transform .3s ease, border-color .3s ease, box-shadow .3s ease;
}

.thread-step:hover .thread-card {
    transform: translateX(4px);
    border-color: rgba(167, 119, 227, 0.4);
    box-shadow: 0 18px 34px -22px rgba(0, 0, 0, 0.8);
}

.thread-step.highlight .thread-card {
    border-color: rgba(167, 119, 227, 0.35);
    background: linear-gradient(180deg, rgba(167, 119, 227, 0.10), var(--bg-2));
}

.thread-step-label {
    display: inline-block;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-faint);
    margin-bottom: 8px;
}

.thread-step-label.accent-label {
    color: var(--accent-soft);
}

.thread-card h3 {
    font-size: 1.3rem;
    margin-bottom: 8px;
}

.thread-card p {
    color: var(--text-secondary);
    font-size: 0.98rem;
}

@media (max-width: 560px) {
    .thread-step {
        gap: 16px;
    }

    .thread-line {
        left: 27px;
    }

    .thread-node {
        width: 44px;
        height: 44px;
        font-size: 1rem;
    }

    .thread-card {
        padding: 18px 20px;
    }
}

/* ---------- Bottom CTA ---------- */
.bottom-cta {
    padding: 100px 0;
}

.cta-box {
    max-width: 900px;
    margin: 0 auto;
    text-align: center;
    background: linear-gradient(160deg, rgba(110, 142, 251, 0.12), rgba(167, 119, 227, 0.12));
    border: 1px solid var(--line-strong);
    border-radius: 28px;
    padding: 70px 32px;
    position: relative;
    overflow: hidden;
    backdrop-filter: blur(8px);
}

.cta-box::before {
    content: '';
    position: absolute;
    inset: 0;
    background: radial-gradient(500px 260px at 50% -10%, rgba(167, 119, 227, 0.22), transparent 70%);
}

.cta-box>* {
    position: relative;
    z-index: 1;
}

.cta-box h2 {
    font-size: clamp(2rem, 4vw, 3rem);
    margin-bottom: 14px;
}

.cta-box p {
    color: var(--text-secondary);
    font-size: 1.12rem;
    margin-bottom: 32px;
}

/* ---------- Footer ---------- */
footer {
    border-top: 1px solid var(--line);
    padding: 56px 0 40px;
}

.footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 22px;
    margin-bottom: 30px;
}

.footer-logo {
    font-family: var(--font-display);
    font-size: 1.3rem;
    font-weight: 800;
}

.footer-links {
    display: flex;
    flex-wrap: wrap;
    gap: 26px;
}

.footer-links a {
    color: var(--text-secondary);
    font-size: 0.92rem;
    transition: color .25s ease;
}

.footer-links a:hover {
    color: var(--accent-soft);
}

.copyright {
    color: var(--text-faint);
    font-size: 0.85rem;
}

/* ---------- Reveal on scroll ---------- */
.reveal {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1);
}

.reveal.in {
    opacity: 1;
    transform: none;
}

/* =========================================================
   Auth Modal (kept functional, matches product theme)
   ========================================================= */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(12, 10, 18, 0.8);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 1;
    transition: opacity .3s;
}

.modal-overlay.hidden {
    opacity: 0;
    pointer-events: none;
}

.modal-card {
    background: linear-gradient(145deg, rgba(32, 26, 48, 0.96), rgba(21, 18, 30, 0.96)) padding-box,
        linear-gradient(145deg, rgba(167, 119, 227, 0.5), rgba(110, 142, 251, 0.2)) border-box;
    border: 1px solid transparent;
    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5), 0 0 60px rgba(167, 119, 227, 0.15);
    border-radius: 22px;
    width: 90%;
    max-width: 430px;
    padding: 38px;
    position: relative;
    transform: translateY(0) scale(1);
    transition: transform .4s cubic-bezier(.175, .885, .32, 1.275);
}

.modal-overlay.hidden .modal-card {
    transform: translateY(16px) scale(.96);
}

.close-btn {
    position: absolute;
    top: 20px;
    right: 20px;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 1px solid var(--line);
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-secondary);
    cursor: pointer;
    display: grid;
    place-items: center;
    transition: all .25s ease;
}

.close-btn:hover {
    background: rgba(255, 80, 80, 0.2);
    color: #fff;
    transform: rotate(90deg);
}

.modal-header h2 {
    font-size: 1.9rem;
    margin-bottom: 6px;
    background: var(--accent-gradient);
    background-clip: text;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    font-weight: 800;
}

.modal-brand-mark {
    width: 52px;
    height: 52px;
    border-radius: 15px;
    display: grid;
    place-items: center;
    margin: 0 auto 16px;
    font-size: 1.35rem;
    color: #fff;
    background: var(--accent-gradient);
    box-shadow: 0 10px 26px -8px rgba(167, 119, 227, 0.7);
}

.modal-header p {
    color: var(--text-secondary);
    margin-bottom: 26px;
    text-align: center;
}

.modal-header h2 {
    text-align: center;
}

.auth-form {
    display: flex;
    flex-direction: column;
    gap: 18px;
}

.input-group {
    display: flex;
    flex-direction: column;
    gap: 7px;
    text-align: left;
}

.input-group label {
    font-size: 0.86rem;
    font-weight: 500;
    color: var(--text-secondary);
}

.input-wrap {
    position: relative;
    display: flex;
    align-items: center;
}

.input-wrap .input-icon {
    position: absolute;
    left: 15px;
    color: var(--text-faint);
    font-size: 0.9rem;
    pointer-events: none;
    transition: color .25s ease;
}

.input-group input {
    width: 100%;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid var(--line);
    padding: 13px 15px 13px 42px;
    border-radius: 12px;
    color: var(--text-primary);
    font-family: var(--font-body);
    font-size: 1rem;
    outline: none;
    transition: all .25s ease;
}

.input-group input::placeholder {
    color: var(--text-faint);
}

.input-group input:focus {
    border-color: var(--accent-color);
    background: rgba(0, 0, 0, 0.5);
    box-shadow: 0 0 0 3px rgba(167, 119, 227, 0.16);
}

.input-wrap:focus-within .input-icon {
    color: var(--accent-soft);
}

.input-hint {
    font-size: 0.76rem;
    color: var(--text-faint);
    line-height: 1.4;
    margin-top: 1px;
}

.agree-check {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    cursor: pointer;
    font-size: 0.82rem;
    line-height: 1.45;
    color: var(--text-secondary);
    font-weight: 400;
}

/* The gate's checkbox is drawn by hand rather than left to the platform. The
   default control paints an opaque white square on a #151220 sheet, which read as
   an unstyled form field sitting in an otherwise themed modal -- and the touch
   block at the end of this file takes it from 17px to 24px on phones, which made
   that square 40% more prominent rather than less. `accent-color` used to be set
   here and is gone with the native rendering: it only ever tinted the checked
   state, and appearance:none makes it inert.

   The tick is two borders on a rotated box, not an icon font or an SVG, so a
   checked gate never depends on Font Awesome having loaded. The gradient fill is
   the primary signal regardless -- if the tick failed to draw, a filled violet box
   still reads as checked against an empty outlined one. */
.agree-check input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    width: 17px;
    height: 17px;
    /* Not cosmetic, and not removable: this checkbox sits inside .input-group, whose
       `input` rule sets `padding: 13px 15px 13px 42px` to clear the leading field
       icon. A native checkbox ignores padding, so that never mattered -- but
       appearance:none makes this a normal box, and under border-box a width smaller
       than the padding is clamped UP to padding + border. Without this line the
       17/24px square silently becomes a 60x28 rounded rectangle, and the 42/15
       asymmetry drags the centred tick off to one side. */
    padding: 0;
    margin-top: 2px;
    flex-shrink: 0;
    cursor: pointer;
    display: grid;
    place-items: center;
    border: 1.5px solid var(--line-strong);
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.05);
    transition: background .2s ease, border-color .2s ease;
}

.agree-check input[type="checkbox"]::after {
    content: "";
    width: 5px;
    height: 9px;
    border: solid #fff;
    border-width: 0 2px 2px 0;
    border-radius: 1px;
    /* The nudge is in the parent's coordinate space, so it has to come before the
       rotation in the list -- transforms apply right to left, and a translate
       written after rotate(45deg) would move the tick diagonally instead of up.
       A rotated L of border has its visual mass below its geometric centre, so
       place-items:center leaves it looking low without this. */
    transform: translateY(-1px) rotate(45deg);
    opacity: 0;
    transition: opacity .15s ease;
}

.agree-check input[type="checkbox"]:checked {
    background: var(--accent-gradient);
    border-color: transparent;
}

.agree-check input[type="checkbox"]:checked::after {
    opacity: 1;
}

.agree-check input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--accent-color);
    outline-offset: 2px;
}

.agree-check a {
    color: var(--accent-soft);
    font-weight: 600;
}

.agree-check a:hover {
    color: var(--accent-color);
    text-decoration: underline;
}

.pw-toggle {
    position: absolute;
    right: 6px;
    width: 32px;
    height: 32px;
    border: none;
    background: transparent;
    color: var(--text-faint);
    cursor: pointer;
    border-radius: 8px;
    display: grid;
    place-items: center;
    transition: color .2s ease, background .2s ease;
}

.pw-toggle:hover {
    color: var(--accent-soft);
    background: rgba(255, 255, 255, 0.05);
}

/* password field keeps room for the toggle button */
#auth-password {
    padding-right: 44px;
}

.full-width {
    width: 100%;
    margin-top: 8px;
    justify-content: center;
}

.modal-footer {
    text-align: center;
    margin-top: 22px;
    font-size: 0.92rem;
    color: var(--text-secondary);
}

.modal-footer a {
    color: var(--accent-soft);
    font-weight: 600;
}

.modal-footer a:hover {
    color: var(--accent-color);
    text-decoration: underline;
}

.auth-status-box {
    margin: 14px 0 4px;
    padding: 12px 15px;
    border-radius: 12px;
    font-size: 0.92rem;
    font-weight: 500;
    line-height: 1.4;
    display: none;
    align-items: center;
    gap: 10px;
    opacity: 0;
    transform: translateY(-8px);
}

.auth-status-box.show {
    display: flex;
    animation: authPop .4s cubic-bezier(.175, .885, .32, 1.275) forwards;
}

.auth-status-box.hidden {
    display: none;
}

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

.auth-status-box.error {
    background: rgba(255, 80, 80, 0.1);
    color: #ff9b91;
    border: 1px solid rgba(255, 80, 80, 0.28);
}

.auth-status-box.success {
    background: rgba(46, 204, 113, 0.12);
    color: #7ee29c;
    border: 1px solid rgba(46, 204, 113, 0.3);
}

@media (max-width: 560px) {

    /* fixed multi-layer backgrounds are janky / mis-sized on mobile browsers */
    body.landing-body {
        background-attachment: scroll, scroll, scroll;
    }

    /* ---- Navbar: must fit logo + both actions inside ~320px ---- */
    .navbar {
        top: calc(10px + var(--safe-top));
        /* Matches the base rule's inset handling; see the comment there. The
           translate offset is inherited from the base rule and not repeated. */
        width: calc(100% - 20px - var(--safe-left) - var(--safe-right));
        padding: 8px 8px 8px 14px;
        gap: 6px;
        flex-wrap: nowrap;
    }

    .navbar.scrolled {
        padding: 7px 8px 7px 12px;
    }

    .nav-logo {
        flex: none;
        min-width: 0;
    }

    .logo-text {
        font-size: 1.08rem;
    }

    .nav-actions {
        flex: none;
        gap: 6px;
        min-width: 0;
    }

    .nav-actions .login-btn,
    .nav-actions .primary-btn.sm {
        white-space: nowrap;
        flex: none;
    }

    .nav-actions .login-btn {
        padding: 7px 11px;
        font-size: 0.78rem;
    }

    .nav-actions .primary-btn.sm {
        padding: 7px 11px;
        font-size: 0.78rem;
        gap: 6px;
    }

    .wrap {
        padding: 0 20px;
    }

    /* ---- Hero ---- */
    .hero {
        padding: 124px 0 64px;
        padding-top: calc(124px + var(--safe-top));   /* see the base .hero rule */
    }

    .hero-title {
        font-size: clamp(2.1rem, 8.6vw, 2.7rem);
        margin-bottom: 18px;
    }

    .hero-subtitle {
        font-size: 1.02rem;
        margin-bottom: 28px;
    }

    /* stack CTAs full width so they don't read as two mismatched pills */
    .cta-group {
        display: grid;
        grid-template-columns: 1fr;
        gap: 12px;
    }

    .cta-group .primary-btn,
    .cta-group .secondary-btn {
        width: 100%;
        justify-content: center;
        padding: 14px 20px;
        font-size: 0.98rem;
    }

    /* icon was centring against 2-line wrapped text */
    .hero-note {
        align-items: flex-start;
        line-height: 1.5;
    }

    .hero-note i {
        margin-top: 0.28em;
        flex: none;
    }

    /* two loose pills → one segmented control */
    .hero-dots {
        margin-top: 32px;
        gap: 0;
        width: fit-content;
        margin-left: auto;
        margin-right: auto;
        padding: 4px;
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.04);
        border: 1px solid var(--line);
    }

    .hero-dot {
        min-height: 40px;
        padding: 8px 20px 8px 16px;
        border: none;
        background: transparent;
        border-radius: 999px;
    }

    .hero-dot.is-active {
        background: linear-gradient(135deg, rgba(110, 142, 251, 0.30), rgba(167, 119, 227, 0.30));
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
    }

    /* ---- Hero demo: stop the rotated card + floating tags from spilling ---- */
    .demo {
        margin-top: 8px;
    }

    .chat-window,
    .diary-demo .chat-window {
        transform: none;
        border-radius: 18px;
    }

    .demo-glow {
        inset: -20px;
    }

    /* tags sat on top of the chat header — put them in the flow above the card */
    .demo-tag {
        position: static;
        animation: none;
        box-shadow: none;
    }

    .demo-tag.t1,
    .demo-tag.t2 {
        top: auto;
        right: auto;
        bottom: auto;
        left: auto;
    }

    .demo-tag.t1 {
        display: inline-flex;
        margin-bottom: 12px;
    }

    /* one badge is enough on a phone */
    .demo-tag.t2 {
        display: none;
    }

    .chat-body {
        min-height: 250px;
        padding: 18px 14px;
        gap: 12px;
    }

    .msg {
        max-width: 88%;
        font-size: 0.9rem;
    }

    /* ---- Section rhythm ---- */
    .intro-section,
    .gallery-section,
    .process-section {
        padding: 64px 0;
    }

    .bottom-cta {
        padding: 70px 0;
    }

    .section-head {
        margin-bottom: 34px;
    }

    /* ---- Gallery: 2-up like a real browse grid, not one card per screen ---- */
    .gallery-grid {
        grid-template-columns: 1fr 1fr;
        gap: 12px;
    }

    .char-card-body {
        padding: 14px 12px;
    }

    .char-card-body h3 {
        font-size: 1.02rem;
    }

    /* the blurb has no room in a half-width card — name + stats carry it */
    .char-card-body p {
        display: none;
    }

    .char-tag {
        font-size: 0.6rem;
        padding: 2px 7px;
        margin-bottom: 7px;
    }

    .char-meta {
        gap: 9px;
        margin-top: 7px;
        font-size: 0.7rem;
    }

    .char-online {
        top: 9px;
        right: 9px;
        font-size: 0.62rem;
        padding: 3px 7px;
    }

    /* The locked teaser takes the full row so its copy stays readable.

       aspect-ratio is switched OFF here, unlike every other card. Full-row 16/9
       computes to 209px on a 411px phone and the overlay's content needs 255, so
       the ratio was not describing the card, it was constraining it: everything
       inside is flex-shrink: 1, so the badge collapsed 46 -> 19px into an ellipse
       and the CTA 36 -> 18px, under even WCAG 2.5.8's 24px floor.

       Letting the content set the height is what fixes it, and it is why
       .char-lock-body below is taken out of the absolute layer. The two changes
       only work together -- an absolutely positioned overlay contributes no
       height, so on its own this rule would collapse the card instead.

       Neither `min-height: auto` nor `min-height: fit-content` substitutes for
       this while the ratio stays. Both were measured on device: a grid item's
       automatic minimum size is switched off for any box whose overflow is not
       `visible`, and .char-card sets `overflow: hidden` to clip its background
       image; and Chromium resolves a block-axis `fit-content` min-height back to
       `auto`, so it computed as the keyword and still used 208.929px. */
    .char-card.locked {
        grid-column: 1 / -1;
        aspect-ratio: auto;
    }

    /* In flow here, absolutely positioned everywhere else — this is the fix for a
       real crush, not a tidy-up.

       On desktop the card is 3/4 of a ~240px grid cell, so there is always more
       height than the overlay needs and `inset: 0` centring it is free. Full-row
       16/9 is the opposite: 371px of row on a 411px phone computes to 209px, and
       the overlay wants ~255px (badge 46 + heading 30 + copy 61 + button 36, plus
       30 of gaps, 12 of margins and 40 of padding). Everything inside is
       flex-shrink: 1 with min-height: auto, so the two items that must NOT shrink
       were the ones that did: the badge collapsed 46 -> 19px into an ellipse and
       the CTA 36 -> 18px, under even WCAG 2.5.8's 24px floor.

       Taking it out of the absolute layer makes it the card's content, so the
       card's automatic minimum size as a grid item grows past what the aspect
       ratio asked for. `relative` rather than `static` because the z-index below
       is what keeps the text above the .locked::before blur — z-index is ignored
       on a static box, which would bury the whole overlay. `inset: auto` because
       the base rule's `inset: 0` is a no-op under relative positioning and being
       explicit is cheaper than leaving that puzzle behind. */
    .char-lock-body {
        position: relative;
        inset: auto;
        padding: 20px;
    }

    .gallery-cta {
        margin-top: 30px;
    }

    .gallery-cta .secondary-btn {
        width: 100%;
        justify-content: center;
    }

    .section-title {
        font-size: clamp(1.75rem, 7.4vw, 2.2rem);
    }

    .section-subtitle,
    .intro-inner p {
        font-size: 1rem;
    }

    .intro-inner p {
        line-height: 1.75;
    }

    .cta-box {
        padding: 46px 22px;
        border-radius: 22px;
    }

    .cta-box p {
        font-size: 1rem;
    }

    .modal-card {
        padding: 30px 22px;
    }
}

/* Very narrow phones (~320–430px): shorten the CTA label so the bar can't clip */
@media (max-width: 430px) {
    .logo-text {
        font-size: 1rem;
    }

    .nav-actions {
        gap: 6px;
    }

    .nav-actions .label-full {
        display: none;
    }

    .nav-actions .label-short {
        display: inline;
    }

    /* icon + short label is still wider than the bar can spare */
    .nav-actions .primary-btn.sm i {
        display: none;
    }

    .nav-actions .login-btn,
    .nav-actions .primary-btn.sm {
        padding: 7px 12px;
        font-size: 0.76rem;
    }
}

/* ═══════════════════════════════════════════════════════════════════
   TOUCH INPUT — marketing pages
   The sibling of the TOUCH INPUT block at the end of styles.css; that one
   covers create_character / dashboard / discover, this one covers the 15
   pages that link landing.css instead. Same reasoning applies — see that
   block's header for why this is keyed on pointer capability rather than a
   width breakpoint, and why both `hover: none` and `pointer: coarse` are
   listed. Keep the two in step.
   ═══════════════════════════════════════════════════════════════════ */

@media (hover: none), (pointer: coarse) {

    /* A tap leaves :hover applied until the next tap lands elsewhere, so each
       of these lifts would freeze mid-air. Only `transform` is reset; the
       box-shadows stay, being invisible next to a stuck 3px lift.
       `.char-card:hover .char-hover` is a transform-only nudge, not an
       opacity reveal, so resetting it hides nothing. */
    .char-card:hover,
    .char-card:hover .char-hover,
    .chat-window:hover,
    .close-btn:hover,
    .diary-demo .chat-window:hover,
    .primary-btn:hover,
    .primary-btn:hover i,
    .secondary-btn:hover,
    .thread-step:hover .thread-card,
    .thread-step:hover .thread-node {
        transform: none;
    }

    /* Chrome only — long-press callouts and stray text selection on the nav and
       CTAs read as "web page". Article copy, legal text and blog bodies are
       deliberately absent: this is the easy way to silently break copying. */
    .navbar,
    .nav-logo,
    .nav-links a,
    .primary-btn,
    .secondary-btn,
    .close-btn,
    .login-btn {
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        user-select: none;
    }

    /* ---- Tap targets ----
       Measured on an API 36 device against --tap-min: 44px (tokens.css:54): both
       nav buttons came back 29-30px tall, the hero dots 40px, and the locked
       card's CTA 36px. Only height is set -- all of them are already wider than
       the minimum, and widening the nav pair is exactly what the two mobile
       breakpoints above spent their effort avoiding, since logo + both actions
       have to fit inside ~320px.

       .primary-btn.sm and .login-btn are unscoped rather than nailed to
       .nav-actions because the set is known and small: one .primary-btn.sm in the
       navbar of each of the 15 marketing pages, one more in the locked teaser
       card, and .login-btn only in the index navbar. Scoping would have meant two
       selectors to say the same thing and would have missed the teaser.

       Keyed on pointer capability rather than a width breakpoint on purpose: the
       desktop navbar keeps its compact 30px buttons, where they are being clicked.
       44px there would push the floating pill 14px taller for no benefit. This is
       also why the .hero padding above had to learn about --safe-top first -- a
       taller navbar eats the clearance below it.

       Deliberately NOT raised: the inline links inside paragraph copy, which
       measure 20px. WCAG 2.5.8 exempts a target that is inline in a sentence, and
       padding one out to 44px either overlaps the lines around it or opens a
       visible gap mid-paragraph. */
    .login-btn,
    .primary-btn.sm,
    .hero-dot {
        min-height: var(--tap-min);
    }

    /* The navbar's home link, 19px tall because it is a bare <a> wrapping a
       <span>. Only the 14 sub-pages have it -- index.html leaves the same logo
       unwrapped, since you are already home -- which is why the landing page
       audited clean while every other page was one short. inline-flex gives
       min-height something to centre against without changing the logo's width
       or the navbar's height, which the 44px buttons already set. */
    .nav-logo a {
        display: inline-flex;
        align-items: center;
        min-height: var(--tap-min);
    }

    /* ---- Auth modal ----
       The login sheet is the first thing a phone user touches, and it audited as
       the worst screen on the marketing side -- but only while it is OPEN, which is
       why the earlier ten-page sweep reported these pages clean: the overlay is
       display:none until something opens it, so every control inside it has a zero
       rect and the audit skips it. Measured at 360px with the sheet up:

         .close-btn             33x33   icon button, standalone
         .pw-toggle             31x31   icon button inside the password field
         #forgot-password-link  122x17  alone on its own row
         "Sign up"               51x17  inline in a sentence

       The two icon buttons keep the size they are drawn at and grow only their hit
       area, through an absolutely-positioned ::after. Both have a visible circle or
       rounded square that is part of the sheet's look, and taking the close button
       from 34 to 44 would have set a chunky disc against 22px of card padding. An
       `inset` of half the shortfall centres the larger region on the smaller
       graphic. Neither button needs position:relative adding -- both are already
       absolutely positioned, so each is its own containing block, and ::after is
       out of flow so it does not disturb the centred icon.

       #forgot-password-link is the one real WCAG 2.5.8 failure in the set: it sits
       alone on its own row, so the inline-in-sentence exemption that covers "Sign
       up" and the paragraph links above does not reach it, and 17px is under the
       24px floor as well as under --tap-min. inline-flex + min-height is the same
       treatment .back-link gets in pages.css.

       "Sign up" stays inline, consistent with those paragraph links -- but it is
       the only route to registration, so it gets padding rather than nothing.
       Padding on an inline box expands the hit region without reflowing the line it
       sits in, and .modal-footer a draws no background, so 17px of text becomes a
       33px target while the sentence renders exactly as before. */
    .close-btn::after,
    .pw-toggle::after {
        content: "";
        position: absolute;
    }

    .close-btn::after { inset: -5px; }   /* 34 + 2*5 = 44 */
    .pw-toggle::after { inset: -6px; }   /* 32 + 2*6 = 44 */

    .modal-footer a {
        padding: 8px 6px;
    }

    #forgot-password-row a {
        display: inline-flex;
        align-items: center;
        min-height: var(--tap-min);
    }

    /* The signup gate's checkbox, 17px square -- the smallest control in the sheet
       and the one that blocks the button. The wrapping <label class="agree-check">
       measures 278x60, because the sentence beside it wraps to three lines, so
       tapping the copy already toggles the box; 24px is so the box itself clears
       WCAG 2.5.8's floor on its own rather than resting on that label. It is the
       standard mobile checkbox size and aligns to the first line of the sentence,
       which the existing `align-items: flex-start` and 2px offset already handle.

       The three policy links inside that same label are left at 17px on purpose.
       They are inline in a sentence, so the exemption that covers "Sign up" covers
       them too -- and unlike "Sign up" they are NOT given hit padding, because the
       area around them belongs to the label: padding a link out vertically here
       would overlap the wrapped lines above and below it and turn part of the
       checkbox's own target into a navigation away from signup. */
    .agree-check input[type="checkbox"] {
        width: 24px;
        height: 24px;
    }
}
