/* ==========================================================================
   ZaapIn discovery cards — shared styles for the modular card set.

   Four components live here: the Google-search card, the AI-assistants card,
   the Social card, and the phone-framed profile mock. Any page can adopt them
   by loading this file plus components/zaapin-cards.js and calling
   ZaapInCards.mount(). Nothing in here assumes /licensed.

   Built for zaapinweb#2 (round 3 addenda 1-3). First adopter is /licensed;
   zaapinweb#3 moves the homepage onto the same modules.

   Class prefix is `zi-` so these never collide with a host page's own styles.
   ========================================================================== */

/* --------------------------------------------------------------------------
   The Discovery widget. Taken from os.html, not approximated: the class names,
   the CSS and the animation loop are the same, so the two files can be diffed.
   Any change here should be made in os.html too, or the two drift.
   -------------------------------------------------------------------------- */
.discovery-card {
    display: flex;
    flex-direction: column;
}
.discovery-search-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    background: rgba(255, 255, 255, 0.7);
    border-bottom: 1px solid rgb(241 245 249);
    font-size: 12px;
    color: rgb(71 85 105);
    font-family: -apple-system, system-ui, sans-serif;
    flex-shrink: 0;
    min-height: 38px;
}
.discovery-query-text {
    font-weight: 500;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
}
.discovery-cursor {
    display: inline-block;
    width: 1.5px;
    height: 12px;
    background: rgb(71 85 105);
    animation: discCursorBlink 1.05s steps(2, start) infinite;
    margin-left: 1px;
    vertical-align: middle;
}
@keyframes discCursorBlink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }

.discovery-result-area {
    position: relative;
    overflow: hidden;
    flex: 1;
    min-height: 180px;
}
.discovery-result {
    padding: 16px;
    transform: translateY(105%);
    opacity: 0;
    transition: transform 0.85s cubic-bezier(0.4, 0, 0.1, 1), opacity 0.6s ease-out;
}
.discovery-result.is-visible {
    transform: translateY(0);
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .discovery-result { transform: translateY(0); opacity: 1; transition: none; }
    .discovery-cursor { animation: none; }
}
/* ===================== end reused block ===================== */

/* JavaScript off: the reused widget's result is animated in, so on its own it
   stays at opacity 0 and translated out of a clipping box — the card would read
   as an empty panel. Kept OUTSIDE the reused block so that block stays
   identical to os.html. */
html:not(.zi-js) .discovery-result { transform: none; opacity: 1; }
html:not(.zi-js) .discovery-cursor { display: none; }

/* --------------------------------------------------------------------------
   The card shell
   -------------------------------------------------------------------------- */
.zi-card {
    background: linear-gradient(to bottom right, #ffffff, #f8fafc 55%, #f5f3ff);
    /* Measured against the site's own cards (index.html and os.html render
       `0 4px 6px -1px rgba(0,0,0,0.05)` — a SOFT elevation). This page ran
       `0 24px 60px -20px rgba(3,7,30,0.55)`, far heavier, which was drift
       rather than a decision. A soft light shadow is invisible on the ruled
       navy ground, so the elevation is kept but pulled back towards the site's
       weight instead of shouting. */
    box-shadow: 0 10px 24px -8px rgba(3, 7, 30, 0.35);
}

/* --------------------------------------------------------------------------
   Social card: the platforms rotate inside one card
   -------------------------------------------------------------------------- */
/* Every slide occupies ONE grid cell, so the rotator is always as tall as its
   TALLEST slide and the card CANNOT change height as the platforms rotate.
   Before this only the active slide was in flow and the box resized to whichever
   platform was showing — Google Maps carries an extra line, so the whole card
   grew when it came round, mid-animation, which reads as the layout breaking.
   Grid stacking gets the max-content height with no measurement and no JS. */
.zi-social { display: grid; }
.social-slide {
    grid-area: 1 / 1;
    opacity: 0;
    transform: translateY(10px);
    transition: opacity .55s ease, transform .55s ease;
    pointer-events: none;
}
/* The showing slide rejoins normal flow so the box sizes to it; the rest stay
   absolute and out of flow. This is what lets the card hug its content on
   desktop while still growing to fill a uniform card on mobile — one rule, no
   measurement, correct at every width. */
.social-slide.is-active { opacity: 1; transform: translateY(0); pointer-events: auto; }
/* JavaScript off: nothing rotates, so the slides are listed instead of stacked. */
html:not(.zi-js) .zi-social { display: block; }
html:not(.zi-js) .social-slide { opacity: 1; transform: none; margin-bottom: 12px; }
@media (prefers-reduced-motion: reduce) {
    .social-slide { transition: none; }
}

/* The phone is narrower than its mock, so the leftover space has to be split
   rather than left to fall where it may. The mock is `flex justify-center`,
   which centred it on desktop — but the mobile rule re-declares that same mock
   as `flex-direction: column`, where `justify-content` governs the VERTICAL
   axis, so the horizontal centring was silently discarded and `max-w-[300px]`
   parked the phone at the start. It read as centred at 360 only because the
   mock is narrower than the cap there, so there was no free space to mis-place.
   `margin-inline: auto` centres a flex item on the cross axis too, so this
   holds whichever direction the parent is running in.
   -------------------------------------------------------------------------- */
.zi-phone-wrap { margin-inline: auto; }

/* --------------------------------------------------------------------------
   The "next card" control — grey glass, right edge, mobile only.

   Rendered by the script only where a next card exists, so it can never point
   at nothing. Hidden by default and switched on inside the mobile block: the
   desktop column has no use for it, since there the cards are simply scrolled
   past like any other page content.
   -------------------------------------------------------------------------- */
.zi-next { display: none; }

/* --------------------------------------------------------------------------
   Scrollbars hidden on the phone body and the mobile row
   -------------------------------------------------------------------------- */
.zi-no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.zi-no-scrollbar::-webkit-scrollbar { display: none; }

/* --------------------------------------------------------------------------
   The sequence container.

   Desktop: a vertical column. Mobile: a horizontal row that snaps one full-width
   card at a time — no peek. That a reader can tell there are more cards is the
   arrival nudge's job, in the script.
   -------------------------------------------------------------------------- */
.zi-cards {
    display: flex;
    flex-direction: column;
    gap: 2rem;
}

/* The widget owns its own desktop behaviour, so the experience is the same
   wherever it is mounted. `html.zi-has-cards` is set BY the mount, so a page
   that never mounts the widget is never given page-level snapping it did not
   ask for. `--zi-align` is written by the same mount, so the CSS snap and the
   script's scroll target read ONE number — an earlier build had them disagree,
   and the snap silently won. */
@media (min-width: 1024px) {
    .zi-cards { gap: 4rem; }
    html.zi-has-cards {
        scroll-snap-type: y proximity;
        scroll-padding-top: var(--zi-align, 8rem);
    }
    .zi-card { scroll-snap-align: start; }

    /* Cards are capped to their own heading. Measured, the four heading blocks
       end at 385-409px; plus the card's 28px padding either side that is ~460px.
       Stretched to a full 720px column the mocks floated in empty ground — the
       phone worst, at 300px of device inside 662px of card. Capping to the
       heading is Vamsi's rule and it happens to land where the phone already
       was. Mobile is untouched: there the cards already fill their row. */
    .zi-card {
        max-width: 460px;
        margin-inline: auto;
    }

    /* Cards are NOT forced to one height — and as of the no-peek ruling this is
       true on BOTH layouts, for one reason: neither shows two cards at once any
       more. Uniformity was only ever worth its cost while the next card was
       visible beside the current one. Remove the peek and the cost (dead white,
       and a tallest card that outgrew the screen) buys nothing at all. */
}

/* Slides align to the TOP of their box. They were briefly centred, which left
   the content floating with equal gaps above and below. */
.social-slide { display: flex; flex-direction: column; justify-content: flex-start; }
html:not(.zi-js) .social-slide { display: block; }

/* The social card's rotator FILLS its mock, the way the discovery widget's
   result area already does via its own `flex: 1`. Without this the box kept its
   min-height and floated inside a taller card — which is what made the social
   card the odd one out while the others looked fine for a reason that was never
   actually shared. */
.card-mock > .zi-social { flex: 1 1 auto; }

@media (max-width: 1023px) {
    .zi-cards {
        flex-direction: row;
        flex-wrap: nowrap;
        /* Cards take their CONTENT height. They were briefly stretched to a
           common height, which was right while the row showed two cards at once
           — but the uniform value is set by the DENSEST card, and at 360px that
           was 797px inside an 800px window, so the tallest card could not be
           seen whole and the other three carried 61-326px of padding each.
           Uniformity only ever mattered because the cards were seen together;
           the hairline peek below means they no longer are. Same reasoning that
           put the desktop column on natural heights in step 22. */
        align-items: flex-start;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scroll-snap-type: x mandatory;
        scroll-behavior: smooth;
        /* One number drives the row's side inset AND its gap, because the
           zero-peek condition is `gap >= side padding`: the next card starts at
           (side + cardWidth + gap), and the card is (viewport - 2*side), so the
           next card lands exactly on the screen edge only while the two are
           equal. Setting them separately is what left a 4px sliver showing when
           the gap was 12px against a 16px bleed.

           The inset also caps how wide a card may get. On a phone the expression
           goes negative and `max()` falls back to the host's bleed, so phones
           are untouched at full width. From ~492px up it takes over and holds
           the card at 460px — the SAME cap round 4 measured for desktop, so a
           tablet gets the desktop card centred on the ground rather than a
           736px box carrying content laid out for ~350px. That was the state
           at 768: the phone mock sat in 190px of white either side.

           The cap cannot be a `max-width` on the card itself — a card narrower
           than the row reintroduces the peek. It has to be the ROW that insets. */
        --zi-side: max(var(--zi-bleed, 0px), calc((100vw - 460px) / 2));
        gap: var(--zi-side);
        /* Cards snap to their START edge, which is what keeps the peek a
           constant width at every phone size. Centre snapping splits the
           leftover space in two and shows a sliver of the PREVIOUS card as
           well — noisier, and it says "more" in both directions when only one
           of them is true. */
        scroll-padding-inline-start: var(--zi-side);
        /* Edge-bleed is OPT-IN. Hard-coding `-1rem` assumed the host was
           full-width with exactly 1rem of padding — true on /licensed, false on
           a centred or max-width container, where it pushed the row past the
           viewport and gave the whole PAGE a horizontal scrollbar. Hosts that
           want the row to reach the screen edges pass `bleed`. */
        margin-inline: calc(-1 * var(--zi-bleed, 0px));
        padding-inline: var(--zi-side);
        /* ROOM FOR THE ELEVATION TO BE SEEN. `overflow-x: auto` forces
           `overflow-y: auto`, so this row CLIPS vertically — and with
           `align-items: flex-start` the tallest card's bottom edge sits flush
           against that clip line. Measured at 360 and 390: the tallest card had
           0px below it inside the row while its shadow
           (`0 10px 24px -8px`) reaches ~26px further down, so the card's bottom
           corners looked cut off rather than lifted.
           28px clears the shadow's full reach. Only the block END needs it: that
           shadow has a +10px y-offset and a -8px spread, so nothing of it is
           painted above the card.
           Why this went unseen through /licensed's whole build: the shadow is
           NAVY and /licensed's card band is midnight, where a dark shadow on a
           dark ground is invisible whether it is clipped or not. It took the
           light homepage to show it — UX constitution 28 from the other side
           (Vamsi, 2026-08-17). */
        padding-block-end: 28px;
        -ms-overflow-style: none;
        scrollbar-width: none;
    }
    .zi-cards::-webkit-scrollbar { display: none; }

    .zi-cards > .zi-card {
        /* NO peek. A 12px hairline was tried on the reasoning that it would read
           as an edge rather than as a card whose height you can judge — that was
           a judgement, it was not measured, and it was wrong. With the cards on
           natural heights the sliver ends at a different point from the card in
           front of it, which puts the unevenness back on screen in the one place
           it is most obvious. Vamsi flagged it from a phone: "it again brought
           the old thing I flagged, bad heights."
           One card, edge to edge. Discovery is the arrival nudge's job. */
        flex: 0 0 100%;
        /* Load-bearing. A flex item's `min-width` defaults to `auto`, so it
           refuses to shrink below its content's intrinsic minimum — and
           `.discovery-query-text` is `white-space: nowrap`, so a long query
           forced the discovery cards to ~470px inside a 390px screen and they
           could never be seen whole. This lets the basis win and the query
           clip, which its own `overflow: hidden` already exists to do. */
        min-width: 0;
        scroll-snap-align: start;
        display: flex;
        flex-direction: column;
    }

    /* With the cards on natural heights the mock no longer has slack to absorb,
       but it keeps `flex: 1 1 auto` so a host that DOES impose a height still
       gets a mock that fills rather than one stranded mid-card. */
    .zi-cards > .zi-card > .card-mock {
        flex: 1 1 auto;
        min-height: 0;
        display: flex;
        flex-direction: column;
        /* EVERY mock starts at the top, the phone included. It was briefly
           centred on the reasoning that a fixed-size device is an object rather
           than a panel; in practice that just floated it away from the header
           it belongs under. */
        justify-content: flex-start;
    }

    /* The Google card is much the densest, and on natural heights it is the one
       card that can outgrow the screen. Three nearby advisors on a phone rather
       than four. */
    .zi-nearby li:nth-child(n+4) { display: none; }

    /* The control anchors to its own card, so it centres itself whatever that
       card's height is — no JS repositioning as the sequence moves. */
    .zi-cards > .zi-card { position: relative; }

    .zi-next {
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: 50%;
        /* Anchored to the SCREEN's right edge, not the card's. The card's right
           edge sits `--zi-side` in from the viewport, so pulling the control out
           by that same amount lands it a constant 8px from the screen edge at
           every width — one expression, no breakpoint. On a tablet that puts it
           out on the navy ground beside the card, which is where there is room
           for it; on a phone, where the inset is only the 16px bleed, it lands
           where it already was. The card is `overflow: visible`, and the control
           sits inside the row's existing gap, so nothing is clipped and the
           row's scroll extent is unchanged. */
        right: calc(8px - var(--zi-side, 6px));
        transform: translateY(-50%);
        /* 30px visual, measured: at 34px the control clipped the right edge of
           the social card's "Contact info" and "Link in bio" labels by 4px. At
           30px it overlaps no text on any of the three cards. */
        width: 30px;
        height: 30px;
        padding: 0;
        border-radius: 999px;
        color: rgb(51 65 85);
        background: rgba(148, 163, 184, 0.20);
        border: 1px solid rgba(148, 163, 184, 0.34);
        -webkit-backdrop-filter: blur(8px) saturate(1.15);
        backdrop-filter: blur(8px) saturate(1.15);
        box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
        transition: background .18s ease, opacity .18s ease;
        /* Hidden by default; the script reveals it on the card in view. Opacity
           rather than `display`, so it fades both ways, and `pointer-events`
           so a hidden control cannot be tapped. */
        opacity: 0;
        pointer-events: none;
    }
    .zi-card.zi-is-current .zi-next { opacity: 1; pointer-events: auto; }
    .zi-next:active { background: rgba(148, 163, 184, 0.34); }

    /* Once the control is clear of the card it sits on the PAGE's ground, so
       its colour is the host's business. Measured: it is fully off the card
       when `--zi-side >= 38px`, i.e. viewport >= 536, so the switch is at 540 —
       below that it still overlaps the white card and keeps the dark treatment
       either way. This block applies only to a host that declared
       `ground: 'dark'`; on a normal light page the dark glyph stays, because a
       white glyph on a white background is invisible. */
    @media (min-width: 540px) {
        .zi-ground-dark .zi-next {
            color: #fff;
            background: rgba(255, 255, 255, 0.14);
            border-color: rgba(255, 255, 255, 0.28);
            box-shadow: 0 2px 10px rgba(3, 7, 30, 0.35);
        }
        .zi-ground-dark .zi-next:active { background: rgba(255, 255, 255, 0.26); }
    }

    /* The visual is 30px so it clears the card's text; the TAP TARGET must not
       shrink with it. This pseudo-element extends the hit area to 44px, the
       accepted minimum, without changing anything that is drawn. */
    .zi-next::before {
        content: "";
        position: absolute;
        inset: -7px;
        border-radius: inherit;
    }

    .zi-cards > .zi-card > header {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.625rem;
    }
    /* ...except a header carrying a SINGLE mark, which sits beside its title
       instead and gives the card back a line of height. Stacking exists for the
       AI and Social cards' three and four chips; the Google and profile cards
       were paying its cost without having its problem (Vamsi, 2026-08-17).
       The class is set by the module from the mark count, so a new single-mark
       card gets this automatically and a multi-mark one cannot get it by
       accident. Matches the desktop row exactly: same direction, same 0.75rem. */
    .zi-cards > .zi-card > header.zi-head-inline {
        flex-direction: row;
        align-items: center;
        gap: 0.75rem;
    }
}

/* On the narrowest common phone (360x800) the visible area after the 54px nav
   is 746px, and the Google card measured 752 — six pixels from being a card no
   reader can ever see whole, with no breathing room at all. One fewer nearby
   advisor here only, since 390 and 412 both fit three with room to spare. The
   rule is keyed to the width that fails rather than applied to every phone. */
@media (max-width: 380px) {
    .zi-nearby li:nth-child(n+3) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
    .zi-cards { scroll-behavior: auto; }
}
