/* Public per-operator landing page. Mobile-first; on wider viewports the
 * page centers in a phone-sized column. Operator's brand color flows in
 * via --md-sys-color-primary (set by App\Twig\ThemeExtension).
 *
 * Color/shape/typescale values come from the M3 token system in
 * md3-theme.css (and per-tenant overrides in theme-*.css). Sizes that
 * don't have an exact M3-baseline match are kept as raw px on purpose —
 * they're the brand-mockup choices for the landing card stack and
 * shouldn't drift if the global typescale tokens change. */

/* iOS 26 (Liquid Glass) samples the toolbar / status-bar tint from the
 * page CSS itself, ignoring <meta name="theme-color">. Default: it reads
 * background-color from html/body. Give <html> the brand primary so any
 * safe-area strip above the viewport paints brand-red instead of the
 * default black. The .landing-body rule below still owns the visible
 * page background — this only shows through in the notch strip and
 * during the splash overlay when the body underneath is temporarily
 * repainted primary via body.has-splash-overlay. */
html {
    background-color: var(--md-sys-color-primary);
    /* Anchor html + body to a real full-screen height so an inset:0
       overlay can fill it end-to-end. Without this iOS Safari collapses
       to the small viewport and lvh alone is not enough on iOS 26. */
    height: 100%;
    height: 100lvh;
    min-height: -webkit-fill-available;
}
body {
    min-height: 100%;
    min-height: 100lvh;
    min-height: -webkit-fill-available;
}

body.landing-body {
    margin: 0;
    /* Operator-controlled via the appearance editor (landing_background field).
     * Falls back to surface-container-lowest (white in light mode) when the
     * operator hasn't picked a custom value. */
    background: var(--app-landing-bg, var(--md-sys-color-surface-container-lowest));
    color: var(--md-sys-color-on-surface);
    font-family: var(--md-sys-typescale-body-large-font);
    min-height: 100vh;
    color-scheme: light;
}

.landing {
    max-width: 480px;
    margin: 0 auto;
    /* With viewport-fit=cover in base.html.twig the viewport extends under
       the notch and home-indicator gutter, so the header + last module
       need the safe-area padding, otherwise they hide behind the iOS chrome. */
    padding: calc(16px + env(safe-area-inset-top, 0px))
             16px
             calc(16px + env(safe-area-inset-bottom, 0px));
    /* Tall enough so the bottom nav (56px bar + 28px FAB lift) doesn't
     * cover the last module on small viewports. */
    padding-bottom: 120px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* Modules live inside .landing-main, so the flex+gap has to live HERE
 * (not on .landing) for hero / promo / grid to be spaced from each other. */
.landing-main {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* ---- Header ---- */
.landing-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 0;
}

.landing-brand {
    display: flex;
    align-items: center;
    gap: 12px;
}

.landing-logo {
    width: 44px;
    height: 44px;
    border-radius: var(--md-sys-shape-corner-full);
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    display: grid;
    place-items: center;
    font-weight: 700;
    /* 20px — between title-large (22) and title-medium (16); kept raw to
     * preserve the brand monogram size without bumping the global scale. */
    font-size: 20px;
    border: 2px solid var(--md-sys-color-primary);
}

/* Uploaded-logo variant of the brand circle. Background matches the
 * operator's primary color (same token as the first-letter fallback above),
 * so the chip reads as a single branded shape regardless of whether the
 * operator has a logo. Logos with transparent backgrounds blend in
 * naturally; logos with their own opaque background hide the color. */
.landing-logo-img {
    object-fit: contain;
    background: var(--md-sys-color-primary);
    padding: 4px;
    border: 2px solid var(--md-sys-color-primary);
}

/* Rectangular variant — picked via Operator::$logoShape on /operator/appearance.
 * Drops the circle's fixed 44x44 square and the colored padded chip so wide
 * wordmarks display at their natural aspect ratio. Height-capped to keep the
 * header row vertically balanced; width auto with a generous max so most
 * wordmarks fit without being constrained. */
.landing-logo.landing-logo-wide {
    width: auto;
    height: 44px;
    max-width: 200px;
    border-radius: var(--md-sys-shape-corner-small);
    /* Drop the colored circle bg + thick border — wordmark logos usually
     * carry their own design and don't want a branded backdrop. */
    background: transparent;
    border: none;
    padding: 0;
}

.landing-name {
    font-family: 'UnifrakturCook', 'Times New Roman', serif;
    /* 24px — operator wordmark; calibrated for iPhone SE so it doesn't
     * dominate the header row. */
    font-size: 24px;
    font-weight: 700;
    letter-spacing: 0.5px;
    color: var(--md-sys-color-on-surface);
}

.landing-profile {
    width: 40px;
    height: 40px;
    border-radius: var(--md-sys-shape-corner-full);
    background: var(--md-sys-color-surface-container-highest);
    color: var(--md-sys-color-on-surface);
    display: grid;
    place-items: center;
    text-decoration: none;
    /* position + overflow scope <md-ripple>'s wave to the circle. */
    position: relative;
    overflow: hidden;
}

/* Logged-in avatar: the user's profile photo fills the 40px circle
 * (overflow:hidden on .landing-profile does the clipping). object-fit:cover
 * keeps any aspect ratio centred and cropped rather than squashed. */
.landing-profile-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* Promo pill replacing the profile icon when operator.headerBadgeText is
 * set. White surface + brand-tinted text + brand-outline. Reads as a
 * call-out tag on the operator's branded header — same vibe as the
 * "-10 % BEI SELF-ORDER" pill in the mockups. Surface tokens are
 * unaffected by the operator's text_color override (light theme stays
 * light), and the brand primary stays at whatever color the operator
 * picked. Inline ripple/focus-ring containers handled via the position
 * + overflow recipe used elsewhere in the header. */
.landing-header-badge {
    display: inline-flex;
    align-items: center;
    padding: 8px 16px;
    border-radius: var(--md-sys-shape-corner-full);
    background: var(--md-sys-color-surface-container-lowest);
    color: var(--md-sys-color-primary);
    font-size: 18px;
    font-weight: 700;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
    line-height: 1;
    /* Same scoping as .landing-profile so md-ripple's wave stays inside. */
    position: relative;
    overflow: hidden;
    transition: background var(--md-sys-motion-duration-short2, 150ms);
}

.landing-header-badge:hover,
.landing-header-badge:focus-visible {
    /* M3 state-layer recipe — 8% primary mixed into the white surface.
     * Just enough tint to confirm the interactive affordance without
     * losing the brand-outlined pill's clean look. */
    background: color-mix(in srgb, var(--md-sys-color-primary) 8%, var(--md-sys-color-surface-container-lowest));
}

/* ---- Hero card (Aktionen & Bundles) ---- */
/* Hero is a positioned block. Text fills the left half (constrained via
 * padding-right so it doesn't overlap the absolute illustration); the
 * illustration is absolutely anchored to the bottom-right corner, flush
 * with the card edges (negative offsets cancel the parent padding). */
.module-hero {
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    border-radius: var(--md-sys-shape-corner-extra-large);
    /* 18px — compact padding sized for iPhone SE (375pt). */
    padding: 18px;
    display: block;
    text-decoration: none;
    overflow: hidden;
    position: relative;
    transition: box-shadow var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.module-hero-text {
    /* No hard width cap — text can flow full card width. The illustration
     * sits absolutely-positioned in the bottom-right corner and visually
     * overlays whatever text would otherwise be there (with transparent
     * PNG silhouettes, text peeks through the gaps); the wrapping <a>
     * keeps the whole card clickable so any obscured text is still
     * reachable. z-index: 1 puts text below the image in stacking order,
     * so the burger draws on top — change to z-index: 2 if you'd rather
     * have text in front. */
    position: relative;
    z-index: 1;
}

.module-hero h2 {
    margin: 0 0 4px;
    /* 26px — hero punch at iPhone SE scale; works with "AKTIONEN &
     * BUNDLES" wrapping to 2 lines if needed. */
    font-size: 32px;
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    line-height: 1.05;
}

.module-hero p {
    margin: 0 0 16px;
    /* 14px — body subtitle, comfortable mobile reading size. */
    font-size: 14px;
    opacity: 0.95;
}

.module-hero-cta {
    display: inline-block;
    background: var(--md-sys-color-on-primary);
    color: var(--md-sys-color-primary);
    padding: 9px 20px;
    border-radius: var(--md-sys-shape-corner-full);
    font-weight: 700;
    /* 13px — pill button label. */
    font-size: 18px;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}

.module-hero-illustration {
    /* Fills the right half of the card, bled past the 18 px padding so
     * the illustration sits flush with the card's outer border edge on
     * three sides (right + top + bottom). The card's overflow: hidden +
     * border-radius cleanly clip any bleed past the rounded corners.
     *
     * Height is derived from top:-18px and bottom:-18px → container
     * stretches to the full card border-box height. With width:55vw
     * (clamped), the container is a tall rectangle on the right side of
     * the card. object-fit: contain on the image scales the burger to
     * fit inside the container at its natural aspect, anchored bottom-
     * right — so the image is ALWAYS as big as the card height allows,
     * never cropped, and adapts to short / tall cards automatically.
     *
     * pointer-events: none keeps the wrapping <a> clickable through the
     * layer. */
    position: absolute;
    /* Right + bottom pinned to the card's content edge (0 = padding-box
     * edge); 18px gap stays between the image and the card's outer
     * border, no bleed past rounded corners. Top still bled past the
     * padding so the fork/tall element of the illustration has room to
     * extend toward the card's top corner. */
    right: 0;
    bottom: 0;
    top: -18px;
    /* Tight cap keeps the image at iPhone-SE proportions on every viewport:
     *   - At 375 px: 45vw = 169 px (matches the mockup design baseline)
     *   - At anything wider: capped at 170 px (so the illustration doesn't
     *     grow with the card — image stays small + proportionate)
     *   - At anything narrower than ~311 px: 140 px floor so it doesn't
     *     shrink to nothing on tiny viewports */
    width: clamp(140px, 45vw, 170px);
    opacity: 0.95;
    pointer-events: none;
}

.module-hero-illustration .material-symbols-outlined {
    /* Material Symbol fallback when no image is uploaded. Matches the
     * container clamp so the glyph fills the box without overflow. */
    font-size: clamp(120px, 38vw, 160px);
    color: var(--md-sys-color-on-primary);
    display: grid;
    place-items: end;
    width: 100%;
    height: 100%;
    line-height: 1;
}

/* Uploaded illustration photo: fills the bounded box, anchored to the
 * bottom-right so the image flushes with the card corner. object-fit:
 * contain preserves the image's natural aspect ratio — wide / tall /
 * square images all fit inside the square box without cropping. */
.module-hero-illustration .module-icon-img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: bottom right;
    display: block;
}

/* Decorative circles in the back of the hero, like in the Redo mock.
 * Two translucent white overlays — a smaller chip near the top right and
 * a larger halo sitting behind the burger illustration. Both kept off
 * the token system: they're compositing tints, not semantic surfaces. */
.module-hero::before {
    content: '';
    position: absolute;
    width: 150px;
    height: 150px;
    border-radius: var(--md-sys-shape-corner-full);
    background: rgba(255, 255, 255, 0.07);
    right: 0px;
    top: 0px;
}

/* Second circle — larger, anchored behind the bottom-right illustration
 * so the burger reads as if it's emerging from a halo. pointer-events:
 * none keeps the wrapping <a> fully clickable through the layer; text
 * + illustration sit on z-index: 1 above this. */
.module-hero::after {
    content: '';
    position: absolute;
    width: 100px;
    height: 100px;
    border-radius: var(--md-sys-shape-corner-full);
    background: rgba(255, 255, 255, 0.08);
    right: 100px;
    bottom: -0px;
    pointer-events: none;
}

/* ---- Promo bar (Essen-Abo) ---- */
/* Uses the operator's main brand color (--md-sys-color-primary) — same token
 * as the hero. Operators think in terms of one brand color; the per-module
 * color override (set via inline style by the landing template) still wins
 * when a module has its own color. M3's "tertiary" surface concept was too
 * subtle for a single-brand landing and led to white-on-white promos when
 * accent was left near-white. */
.module-promo {
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    border-radius: var(--md-sys-shape-corner-large);
    /* 14px — compact mobile padding for the promo bar. */
    padding: 14px;
    display: flex;
    align-items: center;
    gap: 12px;
    text-decoration: none;
    /* position + overflow scope <md-ripple>'s wave to the bar. */
    position: relative;
    overflow: hidden;
    transition: box-shadow var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.module-promo-icon {
    /* 36x36 — matches .module-card-icon so card / promo icons read as
     * the same family at a glance. */
    width: 36px;
    height: 36px;
    border-radius: var(--md-sys-shape-corner-small);
    /* Inverted from the promo surface — the icon sits as a light chip on a
     * brand-colored bar (on-primary as bg, primary as fg). */
    background: var(--md-sys-color-on-primary);
    color: var(--md-sys-color-primary);
    display: grid;
    place-items: center;
    flex-shrink: 0;
}

.module-promo-icon .material-symbols-outlined {
    /* 20px — proportional to the 36px chip; matches .module-card-icon. */
    font-size: 20px;
}

.module-promo-text {
    flex: 1;
}

/* Row container for icon + h3 — keeps them inline. The icon sits to the
 * left of the title text; subtitle below wraps full-width inside
 * .module-promo-text. */
.module-promo-head {
    display: flex;
    align-items: center;
    gap: 10px;
}

.module-promo-text h3 {
    margin: 0;
    font-size: 26px;
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
}

.module-promo-text p {
    margin: -4px 0 0;
    /* 13px — promo subtitle, compact mobile reading. */
    font-size: 13px;
    opacity: 0.92;
    /* Indent to align with the h3 — past the 36px icon chip + the 10px
     * gap inside .module-promo-head. Keeps the title-row icon as the
     * visual anchor for the column. */
    padding-left: 46px;
}

.module-promo-badge {
    background: rgba(255, 255, 255, 0.25);
    color: var(--md-sys-color-on-primary);
    /* 12px — badge label sized small so the pill doesn't overpower the row. */
    font-size: 20px;
    font-weight: 800;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    padding: 6px 12px;
    border-radius: var(--md-sys-shape-corner-small);
    flex-shrink: 0;
}

/* ---- Card grid (Speisekarte / Videos / Games / …) ---- */
.module-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    /* Extra breathing room above the white cards so the colored group
     * (hero + promo) reads as one block, separated from the grid below. */
    margin-top: 16px;
}

/* config.card_width = "full" — the card spans both grid columns while
   keeping the regular card treatment. */
.module-card--full {
    grid-column: 1 / -1;
}

.module-card {
    background: var(--md-sys-color-surface-container-lowest);
    color: var(--md-sys-color-on-surface);
    border-radius: var(--md-sys-shape-corner-large);
    /* 16px — compact padding so each ~160px-wide card on iPhone SE
     * (2-col grid) doesn't feel cramped from the inside. */
    padding: 16px;
    text-decoration: none;
    display: flex;
    flex-direction: column;
    position: relative;
    /* Bound the ripple wave to the card's rounded corners. The .module-card-arrow
     * sits inside the padding, so it's not affected. */
    overflow: hidden;
    /* On a white page background, cards need a subtle border + shadow to read
     * as distinct surfaces. (Originally these sat on a near-black page.) */
    border: 1px solid var(--md-sys-color-outline-variant);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 6px rgba(0, 0, 0, 0.04);
    /* M3 motion: subtle elevation lift on hover via a smoother shadow transition. */
    transition: box-shadow var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.module-card:hover,
.module-hero:hover,
.module-promo:hover {
    box-shadow: var(--md-sys-elevation-level2);
}

.module-card-icon {
    /* 36x36 — compact chip, leaves vertical room on a 150px-tall card. */
    width: 36px;
    height: 36px;
    border-radius: var(--md-sys-shape-corner-small);
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    display: grid;
    place-items: center;
    margin-bottom: 4px;
}

.module-card-icon .material-symbols-outlined {
    /* 20px — proportional to the 36px chip. */
    font-size: 20px;
}

.module-card-title {
    margin: 0;
    font-size: 24px;
    font-weight: 800;
    letter-spacing: 0.3px;
    text-transform: uppercase;
}

.module-card-subtitle {
    margin: 0;
    font-size: 11px;
    color: var(--md-sys-color-on-surface-variant);
    line-height: 1.3;
    /* Reserve space for the absolutely-positioned .module-card-arrow at
     * bottom-right. Padding = arrow size + inset + breathing room so long
     * subtitles wrap before being clipped by the chevron. */
    padding-right: 40px;
}

.module-card-arrow {
    position: absolute;
    /* 12px inset, 26x26 chevron — compact for the 150px card height. */
    bottom: 12px;
    right: 12px;
    width: 26px;
    height: 26px;
    border-radius: var(--md-sys-shape-corner-full);
    border: 1.5px solid var(--md-sys-color-primary);
    color: var(--md-sys-color-primary);
    display: grid;
    place-items: center;
}

.module-card-arrow .material-symbols-outlined {
    font-size: 16px;
    font-weight: 600;
}

/* ---- Module detail page ---- */
/* When a card on the landing grid carries body content, tapping it opens
 * /r/<slug>/m/<id> instead of the operator's external href. Detail pages
 * reuse the same shell (header + footer FABs) so the visitor's mental
 * model — "I'm on Acme's mini-site" — stays intact. */
.landing-brand-link {
    text-decoration: none;
    color: inherit;
}

.module-detail {
    background: var(--md-sys-color-surface-container-lowest);
    border-radius: var(--md-sys-shape-corner-large);
    /* No border — the subtle shadow alone separates the card from the
     * landing background, matching production. Re-enable if a stronger
     * card boundary is needed on light backgrounds. */
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 6px rgba(0, 0, 0, 0.04);
    /* No outer padding — the header band extends to the card edges, and the
     * body gets its own padding below. Keeps the brand-colored strip flush. */
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* Brand-colored band at the top of the detail card, same brand token the
 * landing hero uses. Per-module color override sets --md-sys-color-primary
 * via inline style on the .landing wrapper, so this header automatically
 * picks it up when the module has its own color. */
.module-detail-header {
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    padding: 20px;
    display: flex;
    align-items: center;
    gap: 14px;
}

.module-detail-icon {
    width: 48px;
    height: 48px;
    border-radius: var(--md-sys-shape-corner-medium);
    /* Inverted vs. the header band — a light chip on the brand surface,
     * same pattern the promo icon uses. */
    background: var(--md-sys-color-on-primary);
    color: var(--md-sys-color-primary);
    display: grid;
    place-items: center;
    flex-shrink: 0;
}

.module-detail-icon .material-symbols-outlined {
    font-size: var(--md-sys-typescale-headline-medium-size);
}

.module-detail-heading h1 {
    margin: 0;
    font-size: var(--md-sys-typescale-title-large-size);
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    line-height: 1.15;
}

.module-detail-heading p {
    margin: 4px 0 0;
    font-size: var(--md-sys-typescale-body-medium-size);
    /* `inherit` rides the on-primary color from the header band; opacity
     * softens it so it reads as a subtitle, not as bold as the h1. */
    color: inherit;
    opacity: 0.85;
    line-height: 1.3;
}

.module-detail-body {
    /* Padded inset on the white surface area, below the colored header band. */
    padding: 20px;
    /* 15px — brand-mockup body size, between body-medium (14) and
     * body-large (16). Keeping raw to preserve operator-content readability. */
    font-size: 15px;
    line-height: 1.55;
    color: var(--md-sys-color-on-surface);
    /* nl2br converts plain newlines to <br>; this preserves blank-line gaps. */
    word-break: break-word;
}

/* ---- Signed-in account pages ---- */
/* Full-width title shown above the menu / forms. Sits inside .landing-main,
 * so the 480px column constraint and gap come from the parent. */
.account-page-title {
    margin: 8px 0 0;
    font-size: var(--md-sys-typescale-headline-medium-size);
    font-weight: 700;
    color: var(--md-sys-color-on-surface);
    line-height: 1.2;
}

.account-greeting {
    margin: 0;
    color: var(--md-sys-color-on-surface-variant);
    font-size: var(--md-sys-typescale-body-medium-size);
}

.account-signed-in-as {
    margin-top: 8px;
    color: var(--md-sys-color-on-surface-variant);
    font-size: var(--md-sys-typescale-body-small-size);
    text-align: center;
}

/* Stacked menu of account actions (Profile / Vouchers / Sign out / …).
 * Each row is a pill-shaped tile with M3 ripple + focus-ring; replaces the
 * 2-column dashboard grid on the old /account page. */
/* ============================================================== */
/* On-surface reset for elevated cards                              */
/* ============================================================== */
/* ThemeExtension scopes the operator's text_color to `.landing` and
 * `.page-splash` by overriding `--md-sys-color-on-surface`. That's right
 * for page-level body text (lives directly on the operator's
 * landing-background), but cards inside those wrappers sit on a LIGHT
 * surface-container-lowest background where dark text is needed for
 * contrast — operator's white text_color would invert this and produce
 * white-on-white card text + invisible Material Web input text.
 *
 * Reset the on-surface tokens to their M3 light defaults inside the
 * elevated-card classes so their text + nested M3 components render
 * with the M3 default dark text regardless of the operator's choice.
 * Operator's text_color is for the page background, not for card
 * interiors. */
.card,
.module-card,
.module-detail,
.account-menu-item,
.auth-public-card,
.profile-card {
    --md-sys-color-on-surface: #1D1B20;
    --md-sys-color-on-surface-variant: #49454F;
}

.account-menu {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.account-menu-item {
    display: grid;
    grid-template-columns: 40px 1fr 24px;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
    background: var(--md-sys-color-surface-container-lowest);
    border: 1px solid var(--md-sys-color-outline-variant);
    border-radius: var(--md-sys-shape-corner-large);
    text-decoration: none;
    color: var(--md-sys-color-on-surface);
    /* Ripple needs a positioned, overflow-clipped container. */
    position: relative;
    overflow: hidden;
    transition: box-shadow var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.account-menu-item:hover {
    box-shadow: var(--md-sys-elevation-level1);
}

.account-menu-icon {
    width: 40px;
    height: 40px;
    border-radius: var(--md-sys-shape-corner-small);
    background: var(--md-sys-color-primary-container);
    color: var(--md-sys-color-on-primary-container);
    display: grid;
    place-items: center;
}

.account-menu-icon .material-symbols-outlined {
    font-size: 22px;
    line-height: 1;
}

.account-menu-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.account-menu-text strong {
    font-size: var(--md-sys-typescale-body-large-size);
    font-weight: 600;
    color: var(--md-sys-color-on-surface);
}

.account-menu-text small {
    font-size: var(--md-sys-typescale-body-small-size);
    color: var(--md-sys-color-on-surface-variant);
}

.account-menu-arrow {
    color: var(--md-sys-color-on-surface-variant);
    display: grid;
    place-items: center;
}

.account-menu-arrow .material-symbols-outlined {
    font-size: 22px;
    line-height: 1;
}

/* Sign-out tile gets error tokens so destructive intent reads at a glance. */
.account-menu-item-destructive .account-menu-icon {
    background: var(--md-sys-color-error-container);
    color: var(--md-sys-color-on-error-container);
}

.account-menu-item-destructive {
    --md-ripple-hover-color: var(--md-sys-color-error);
    --md-ripple-pressed-color: var(--md-sys-color-error);
}

/* ---- Welcome-photo onboarding step ---- */
/* Forced step after registration: visitor must pick + crop a selfie
 * before any other protected page becomes accessible. Reuses the
 * image-workflow Stimulus controller; the crop UI is constrained to a
 * 1:1 aspect ratio and masked as a circle so the result reads as a
 * profile avatar instead of a generic image. */
.welcome-photo-card,
.welcome-subscribe-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
    padding: 24px;
    text-align: center;
}

.welcome-subscribe-icon {
    width: 96px;
    height: 96px;
    border-radius: var(--md-sys-shape-corner-full);
    background: var(--md-sys-color-primary-container);
    color: var(--md-sys-color-on-primary-container);
    display: grid;
    place-items: center;
}

.welcome-subscribe-icon .material-symbols-outlined {
    font-size: 48px;
}

/* :not([hidden]) so the HTML `hidden` attribute still resolves to
 * display:none — without this, `display: flex` would override hidden
 * and the editor would show with a broken-image src before any file
 * is picked. */
.welcome-photo-picker:not([hidden]),
.welcome-photo-editor:not([hidden]) {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 16px;
}

.welcome-photo-placeholder {
    width: 144px;
    height: 144px;
    border-radius: var(--md-sys-shape-corner-full);
    background: var(--md-sys-color-surface-container-high);
    color: var(--md-sys-color-on-surface-variant);
    display: grid;
    place-items: center;
    border: 2px dashed var(--md-sys-color-outline-variant);
}

.welcome-photo-placeholder .material-symbols-outlined {
    font-size: 56px;
}

/* Circle mask over Cropper.js's rectangular selection. The underlying
 * crop coords stay rectangular (PNG/JPEG can't carry transparency
 * around the edges), so the watermarked output is square — we just
 * preview it as a circle so the visitor sees the intended avatar
 * shape. CSS isolates the mask to the stage element only.
 *
 * NOTE: Cropper.js wraps the image in its own .cropper-container chrome,
 * so we mask the OUTER stage rather than the inner crop-box. */
.cropper-stage-circle {
    border-radius: var(--md-sys-shape-corner-full);
    overflow: hidden;
    max-width: 320px;
    aspect-ratio: 1 / 1;
    margin: 0 auto;
}

.cropper-stage-circle .cropper-view-box,
.cropper-stage-circle .cropper-face {
    border-radius: var(--md-sys-shape-corner-full);
}

/* Soft "sign me out" escape under the upload card. Muted on-surface-variant
 * so it doesn't compete with the primary "Use this photo" action. */
.welcome-photo-signout {
    text-align: center;
    margin-top: 16px;
}

.welcome-photo-signout a {
    color: var(--md-sys-color-on-surface-variant);
    text-decoration: underline;
    font-size: var(--md-sys-typescale-body-small-size);
}

.welcome-photo-signout a:hover {
    color: var(--md-sys-color-on-surface);
}

/* ---- Operator-scoped auth pages (login / register / forgot-password) ----
 * Mobile-first card that reuses the landing-body chrome for header +
 * background, then drops a centered M3 outlined-card with the form
 * inside. Lives in landing.css (not app.css) because it's part of the
 * public-facing visitor flow, not the operator dashboard. */
.auth-public-main {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 16px;
}

.auth-public-card {
    background: var(--md-sys-color-surface-container-lowest);
    color: var(--md-sys-color-on-surface);
    border-radius: var(--md-sys-shape-corner-large);
    border: 1px solid var(--md-sys-color-outline-variant);
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 6px rgba(0, 0, 0, 0.04);
}

.auth-public-header h1 {
    margin: 0 0 4px;
    font-size: var(--md-sys-typescale-title-large-size);
    font-weight: 700;
    color: var(--md-sys-color-on-surface);
}

.auth-public-header .lede {
    margin: 0;
    color: var(--md-sys-color-on-surface-variant);
    font-size: var(--md-sys-typescale-body-medium-size);
    line-height: 1.4;
}

.auth-public-card .auth-form {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.auth-public-card md-filled-button,
.auth-public-card md-text-button {
    width: 100%;
}

/* Footer links row (Create an account / Forgot password). M3 text buttons
 * give us a hover state-layer + ripple for free. */
.auth-public-links {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-top: 4px;
    align-items: center;
}

.auth-public-links a {
    color: var(--md-sys-color-primary);
    font-size: var(--md-sys-typescale-body-medium-size);
    text-decoration: none;
}

.auth-public-links a:hover {
    text-decoration: underline;
}

/* Sign in / Register entry buttons that show in the landing header for
 * anonymous visitors. Sit alongside the profile-icon avatar (which is
 * hidden for anonymous). M3 tonal style so they read as primary actions
 * without dominating the operator's brand colour. */
.landing-auth-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.landing-auth-actions .auth-pill {
    display: inline-block;
    padding: 8px 16px;
    border-radius: var(--md-sys-shape-corner-full);
    font-size: var(--md-sys-typescale-label-large-size);
    font-weight: 500;
    text-decoration: none;
    line-height: 1;
    transition: background var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.landing-auth-actions .auth-pill-primary {
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
}

.landing-auth-actions .auth-pill-primary:hover {
    box-shadow: var(--md-sys-elevation-level1);
}

.landing-auth-actions .auth-pill-text {
    color: var(--md-sys-color-primary);
}

.landing-auth-actions .auth-pill-text:hover {
    background: color-mix(in srgb, var(--md-sys-color-primary) 8%, transparent);
}

/* ---- Bottom navigation ----
 * Material 3 "bottom app bar with center FAB" pattern. Five slots in a
 * grid: 2 nav items, FAB (lifted above the bar), 2 nav items. White bar
 * regardless of operator branding so the FAB and active item stand out
 * as the operator's primary color. The bar is fixed-position inside the
 * 480px phone column, matching the rest of .landing's max-width. */
.landing-bottomnav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    margin: 0 auto;
    max-width: 480px;
    background: var(--md-sys-color-surface-container-lowest);
    border-top: 1px solid var(--md-sys-color-outline-variant);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
    display: grid;
    /* The partial sets --bottomnav-cols based on how many slots the
     * operator filled (Home + items + FAB). Falls back to 5 — the full
     * Home + 3 items + FAB shape — if the variable is missing. */
    grid-template-columns: repeat(var(--bottomnav-cols, 5), 1fr);
    align-items: end;
    padding: 6px 8px 10px;
    /* Respect iOS safe-area on devices with home-indicator gestures. */
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0));
}

.bottomnav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 8px 4px;
    text-decoration: none;
    color: var(--md-sys-color-on-surface-variant);
    font-size: var(--md-sys-typescale-label-small-size);
    font-weight: var(--md-sys-typescale-label-small-weight);
    letter-spacing: 0.2px;
    /* Truncate long module titles so 5 slots stay laid out evenly. */
    max-width: 100%;
    /* Anchor for <md-ripple> + <md-focus-ring>; the ripple wave gets
     * clipped to a rounded pill that sits inside the slot. */
    position: relative;
    border-radius: var(--md-sys-shape-corner-medium);
    overflow: hidden;
    transition: color var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.bottomnav-item.is-active {
    color: var(--md-sys-color-primary);
}

/* Tint the ripple state-layer with the operator's primary color so hover
 * + press feel like an "M3 navigation bar" tab. Default would be the
 * muted text color (.bottomnav-item.color), which reads as dim gray. */
.bottomnav-item {
    --md-ripple-hover-color: var(--md-sys-color-primary);
    --md-ripple-pressed-color: var(--md-sys-color-primary);
}

.bottomnav-item .material-symbols-outlined {
    font-size: 24px;
}

/* Uploaded icon in a nav slot. There's no fixed-size wrapper around the
 * symbol (just the flex column on .bottomnav-item), so pin an explicit
 * size that matches the Material Symbol rendering above. */
.bottomnav-item > .module-icon-img {
    width: 24px;
    height: 24px;
}

.bottomnav-label {
    line-height: 1;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The center slot lifts above the bar by ~28px. Negative top margin pulls
 * it up; align-self overrides the parent's `align-items: end`. */
.bottomnav-fab {
    width: 56px;
    height: 56px;
    margin: -15px auto 0;
    align-self: start;
    border-radius: var(--md-sys-shape-corner-full);
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    display: grid;
    place-items: center;
    text-decoration: none;
    /* Default elevation; bumps to level3 on hover for a subtle lift. */
    box-shadow: var(--md-sys-elevation-level2);
    /* Anchor for <md-ripple>; the wave stays inside the circle. */
    position: relative;
    overflow: hidden;
    transition: box-shadow var(--md-sys-motion-duration-short2) var(--md-sys-motion-easing-standard);
}

.bottomnav-fab:hover {
    box-shadow: var(--md-sys-elevation-level3);
}

.bottomnav-fab .material-symbols-outlined {
    font-size: var(--md-sys-typescale-headline-medium-size);
    color: var(--md-sys-color-on-primary);
}

/* Uploaded icon in the FAB. Same explicit-size approach as nav items; the
 * FAB's grid container places it centered automatically. */
.bottomnav-fab > .module-icon-img {
    width: 28px;
    height: 28px;
}

/* ============================================================== */
/* Custom Pages — public render                                    */
/* ============================================================== */

/* Inner header on a custom page: chevron-back chip + two-tone H1.
 * Sits at the top of .landing-main, inside the same 480px column the
 * landing modules use, so spacing matches the rest of the public app. */
.page-header {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 4px 0 4px;
}

.page-back {
    position: relative;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    color: var(--md-sys-color-on-surface);
    text-decoration: none;
    overflow: hidden;
    /* Soft hover tint, same pattern as bottom-nav icons. */
    background: color-mix(in srgb, var(--md-sys-color-on-surface) 6%, transparent);
    flex-shrink: 0;
}

.page-back:hover,
.page-back:focus-visible {
    background: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
}

.page-back .material-symbols-outlined {
    font-size: 26px;
}

/* Breadcrumb variant of the back button — outlined, no fill, smaller (~30px).
 * Border + glyph use the neutral M3 outline / on-surface-variant tokens (cool
 * gray, theme-customisable) instead of the filled hover-tint chip. */
.page-back--breadcrumb {
    width: 23px;
    height: 23px;
    background: transparent;
    border: 1px solid var(--md-sys-color-outline);
    color: var(--md-sys-color-on-surface-variant);
    margin-top: 2px
}

.page-back--breadcrumb:hover,
.page-back--breadcrumb:focus-visible {
    background: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, transparent);
}

.page-back--breadcrumb .material-symbols-outlined {
    font-size: 20px;
}

.page-title {
    margin: 0;
    /* Operator appearance settings: inherit the chosen font family (falls back
       to the inherited default when unset) and scale the size by the operator's
       base-size multiplier — the pattern ThemeExtension documents for page
       titles. Carrying these on the class (not just the h1 element rule) means
       the module-detail breadcrumb honors them too, even though it renders as a
       <span> rather than an <h1>. */
    font-family: var(--app-font-family, inherit);
    font-size: calc(26px * var(--app-text-scale, 1));
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    line-height: 1.1;
    color: var(--md-sys-color-on-surface);
    display: flex;
    flex-wrap: wrap;
    gap: 0 8px;
}

/* Accent half of the title — same two-tone pattern the landing wordmark uses,
 * see ".landing-name" in the wordmark block. */
.page-title-accent {
    color: var(--md-sys-color-primary);
}

/* Breadcrumb variant — a slimmer navigation-label treatment of the title.
 * Used by the module detail breadcrumb (where the title is also repeated in
 * the colored card header below) and by the public page header, so both share
 * the same compact look. */
.page-title--breadcrumb {
    font-size: calc(17px * var(--app-text-scale, 1));
    line-height: 1.2;
    letter-spacing: -0.1px;
    margin-left: 0.25rem;
}

/* Element stack — vertical column of mixed-type cards. Operator-uploaded
 * content gets a generous surface; spacing is uniform regardless of type. */
.page-elements {
    display: flex;
    flex-direction: column;
    gap: 20px;
    margin-top: 8px;
}

.page-element {
    margin: 0;
}

/* ----- text element ----- */
.page-element-text p {
    margin: 0 0 12px;
    font-size: 15px;
    line-height: 1.55;
    color: var(--md-sys-color-on-surface);
    word-break: break-word;
}

.page-element-text p:last-child {
    margin-bottom: 0;
}

.page-element-text--center {
    text-align: center;
}

.page-element-text--right {
    text-align: right;
}

/* ----- subheadline element ----- */
.page-element-subheadline {
    margin: 12px 0 0;
    font-size: var(--md-sys-typescale-title-large-size);
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--md-sys-color-on-surface);
    text-align: center;
    line-height: 1.2;
}

/* ----- image element -----
 * Brand-red frame + fixed 21:9 aspect crop, rendered on the <img> itself
 * so the figcaption below stays outside the bordered box. Source images
 * of any natural ratio get center-cropped via object-fit:cover —
 * operators should pick photos with the subject roughly centered. */
.page-element-image {
    margin: 0;
}

.page-element-image img {
    display: block;
    width: 100%;
    aspect-ratio: 21 / 9;
    object-fit: cover;
    border-radius: var(--md-sys-shape-corner-large);
}

.page-element-image figcaption {
    padding: 10px 14px 12px;
    font-size: var(--md-sys-typescale-body-small-size);
    color: var(--md-sys-color-on-surface-variant);
    line-height: 1.4;
}

/* ----- video element ----- */
.page-element-video {
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
}

.page-video-poster {
    position: relative;
    aspect-ratio: 16 / 9;
    border-radius: var(--md-sys-shape-corner-large);
    border:1px solid var(--md-sys-color-primary);
    overflow: hidden;
}

/* "Center" layout: fixed 350×195 poster centered in the page column. Used
 * on article pages where the video sits inline between body copy and the
 * default full-bleed treatment would be too dominant. The fixed width
 * overrides the .page-video-poster aspect-ratio (the explicit height
 * wins). The caption below inherits the centered alignment via the
 * parent flex column. */
.page-element-video--center {
    align-items: center;
}

.page-element-video--center .page-video-poster {
    width: 100%;
    max-width: 350px;
    aspect-ratio: 350 / 195;
    height: auto;
}
.page-element-video--center .page-video-frame {
    width: 100%;
    max-width: 350px;
    aspect-ratio: 350 / 195;
    height: auto;
    border-radius: var(--md-sys-shape-corner-large);
    border-color: 1px solid var(--md-sys-color-primary);
}

/* Embedded player (iframe or <video>) — replaces the poster div in place
 * when video_player_controller#play fires. The same 16:9 frame keeps the
 * layout stable across the swap so the caption underneath doesn't jump. */
.page-video-frame {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    border: 0;
    background: #000;
}


.page-element-story-carousel--story .page-carousel-card > .page-video-frame {
    aspect-ratio: 16 / 10;
}
.page-element-story-carousel--article .page-carousel-card:first-child > .page-video-frame {
    height: 100%;
}
.page-element-story-carousel--article .page-carousel-card:not(:first-child) > .page-video-frame {
    aspect-ratio: 1 / 1;
    height: 100%;
}

/* Portrait embeds (Instagram reels etc.) — the player swap replaces the
   poster element entirely, so this wrapper governs the embed's size. Capped
   and centered so a vertical reel doesn't span a full-width video card or
   fight the 16:9 .page-video-frame aspect rules above. */
.page-video-embed {
    display: block;
    width: 100%;
    margin-inline: auto;
    background: #000;
}
.page-video-embed--portrait {
    max-width: 340px;
}
.page-video-embed-frame {
    display: block;
    width: 100%;
    aspect-ratio: 9 / 16;
    border: 0;
    background: #000;
}

.page-video-poster img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.page-video-poster-fallback {
    position: absolute;
    inset: 0;
    margin: auto;
    width: 64px;
    height: 64px;
    font-size: 64px;
    color: var(--md-sys-color-on-surface-variant);
    display: grid;
    place-items: center;
}

/* Centered play badge on top of the poster, M3 brand-tinted. */
.page-video-play {
    position: absolute;
    inset: 0;
    margin: auto;
    width: 66px;
    height: 66px;
    border-radius: 50%;
    /* background: rgba(0, 0, 0, 0.55); */
    color: #fff;
    display: grid;
    place-items: center;
    pointer-events: none;
    border: 6px solid #fff;
}

.page-video-play .material-symbols-outlined {
    font-size: 42px;
    font-variation-settings: 'FILL' 1;
}

.page-video-duration {
    position: absolute;
    right: 10px;
    top: 10px;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    font-size: var(--md-sys-typescale-label-small-size);
    font-weight: 600;
}

.page-video-caption {
    margin: 0;
    padding: 10px 14px 12px 0px;
    /* font-size: var(--md-sys-typescale-body-small-size); */
    font-size: 15px;
    color: var(--md-sys-color-on-surface-variant);
    line-height: 1.4;
}

/* ============================================================== */
/* Custom Pages — Phase 2 element types                            */
/* ============================================================== */

/* ----- headline element -----
 * Large, left-aligned in-page title. Distinct from .page-element-subheadline
 * (small, centered, uppercase) — the headline is the article title; the
 * subheadline is a section break. */
.page-element-headline {
    margin: 4px 0 0;
    font-size: 36px;
    line-height: 1.05;
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--md-sys-color-on-surface);
    word-break: break-word;
}

/* ----- meta_row element -----
 * "Badge · date" row used above article-style headlines. The badge is a
 * brand-outlined pill; the date is plain on-surface text. */
.page-element-meta-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px;
    margin: 12px 0 0;
}

.page-meta-badge {
    display: inline-flex;
    align-items: center;
    padding: 6px 14px;
    border-radius: 999px;
    border: 1px solid var(--md-sys-color-primary);
    color: var(--md-sys-color-primary);
    font-size: var(--md-sys-typescale-label-medium-size);
    font-weight: 700;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    line-height: 1;
}

.page-meta-date {
    color: var(--md-sys-color-on-surface);
    font-size: var(--md-sys-typescale-body-medium-size);
    line-height: 1.2;
}

/* ----- divider element -----
 * Small uppercase label + flex horizontal rule, e.g. "MEHR STORIES ————".
 * Falls back to a centered rule when label is empty. */
.page-element-divider {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 24px 0 4px;
}

.page-divider-label {
    font-size: var(--md-sys-typescale-label-medium-size);
    font-weight: 700;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    color: var(--md-sys-color-on-surface);
    flex-shrink: 0;
}

.page-divider-rule {
    flex: 1;
    height: 1px;
    background: var(--md-sys-color-outline-variant);
}

/* ----- story_carousel element -----
 * Horizontal-scroll card strip with native scroll-snap. Cards are
 * uniform-width (260px) so the strip reads like a row of "story tiles".
 *
 * min-width: 0 is the critical bit — without it this wrapper is a flex
 * child of .page-elements (a flex column) and gets min-width: auto by
 * default, which propagates the track's min-content (5×260px + gaps
 * ≈ 1348px) upward. That blows past the .landing 480px column AND
 * defeats overflow-x: auto on the track (the track ends up as wide as
 * its content, with nothing to scroll). Pinning min-width to 0 lets
 * align-items: stretch from the parent flex column take over: the
 * wrapper sizes to ~448px and the track scrolls horizontally inside it. */
.page-element-story-carousel {
    margin: 4px 0 0;
    min-width: 0;
}

.page-carousel-track {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    /* Smooth scroll on programmatic scrollTo calls (dot click + auto-
     * advance). The JS-side `behavior: 'smooth'` option alone is
     * inconsistent across browsers when combined with scroll-snap
     * mandatory — Chrome sometimes cancels the smooth animation and
     * jumps to the nearest snap point. Setting `scroll-behavior` in CSS
     * is the more reliable signal that snap should respect. */
    scroll-behavior: smooth;
    /* Hide scrollbar — the dots row below substitutes as the position cue. */
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
    /* Drag affordance: cursor switches to grabbing in JS while dragging.
     * user-select: none prevents the browser from selecting card text/images
     * during a drag — without it a drag turns into a text highlight. */
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
}

.page-carousel-track.is-dragging,
.page-carousel-track.is-settling {
    scroll-snap-type: none !important;
}

.page-carousel-track.is-dragging,
.page-carousel-track.is-settling {
    scroll-behavior: auto !important;
}

.page-carousel-track.is-dragging {
    -webkit-overflow-scrolling: auto;
}

.page-carousel-track::-webkit-scrollbar {
    display: none;
}

.page-carousel-card {
    position: relative;
    flex: 0 0 260px;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    gap: 6px;
    border-radius: var(--md-sys-shape-corner-large);
    text-decoration: none;
    color: var(--md-sys-color-on-surface);
    overflow: hidden;
    /* Belt-and-braces alongside dragstart.preventDefault in
     * carousel_controller.js — some browsers ignore JS-side dragstart
     * suppression on <a> elements without the corresponding CSS hint, and
     * Safari needs -webkit-user-drag specifically. */
    -webkit-user-drag: none;
    user-drag: none;
}

/* Same suppression for the images inside cards — without it the browser
 * fires its native image-ghost drag on mousedown and our drag gesture
 * never gets a mousemove. */
.page-carousel-card img {
    -webkit-user-drag: none;
    user-drag: none;
    pointer-events: none;
}

.page-carousel-thumb {
    position: relative;
    aspect-ratio: 16 / 10;
    background: var(--md-sys-color-surface-container);
    border-radius: var(--md-sys-shape-corner-large);
    overflow: hidden;
}

/* Brand-tinted border on every carousel thumb. Rendered via ::after
 * (not box-shadow:inset) because the inner <img> has object-fit:cover
 * which paints over the thumb's background — the inset shadow ends up
 * hidden under the image. The ::after layer sits ABOVE the image,
 * inherits border-radius so it follows the rounded corners, and is
 * pointer-events:none so it doesn't intercept video-play clicks.
 * Variants override the border-width on this same pseudo-element. */
.page-carousel-thumb::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 1px solid var(--md-sys-color-primary);
    border-radius: inherit;
    pointer-events: none;
}

.page-carousel-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.page-carousel-thumb .material-symbols-outlined {
    position: absolute;
    inset: 0;
    margin: auto;
    font-size: 48px;
    color: var(--md-sys-color-on-surface-variant);
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
}

.page-carousel-play {
    position: absolute;
    inset: 0;
    margin: auto;
    width: 60px;
    height: 60px;
    border-radius: 50%;
    /* background: rgba(0, 0, 0, 0.55); */
    color: #fff;
    display: grid;
    place-items: center;
    pointer-events: none;
    border: 6px solid #fff;
}

.page-carousel-play .material-symbols-outlined {
    /* Override the fallback positioning above — inside the play badge we
     * want the icon centered in the 48px circle, not absolute-pinned. */
    position: static;
    inset: auto;
    width: auto;
    height: auto;
    font-size: 38px;
    color: #fff;
    font-variation-settings: 'FILL' 1;
}

.page-carousel-title {
    margin: 6px 8px 0;
    font-size: var(--md-sys-typescale-title-small-size);
    font-weight: 700;
    line-height: 1.2;
    color: var(--md-sys-color-on-surface);
    word-break: break-word;
}

.page-carousel-subtitle {
    margin: 0 8px 8px;
    font-size: var(--md-sys-typescale-body-small-size);
    color: var(--md-sys-color-on-surface-variant);
    line-height: 1.3;
}

/* Pagination dots — inert decoration. Stimulus-driven active state lands
 * in Phase 2b; for now the first dot is always highlighted as a visual cue. */
.page-carousel-dots {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 8px;
    margin-top: 14px;
}

/* Dots are <button> elements (clickable — the carousel controller scrolls
 * to the matching card on click). Strip default button chrome so they look
 * identical to the inert <span> visuals they replaced. */
.page-carousel-dot {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: transparent;
    transition: background var(--md-sys-motion-duration-short2, 150ms),
                width var(--md-sys-motion-duration-short2, 150ms);
    padding: 0;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    border: 1px solid var(--md-sys-color-primary);

}
/* 
.page-carousel-dot:hover {
    background: var(--md-sys-color-on-surface-variant);
} */

.page-carousel-dot.is-active {
    background: var(--md-sys-color-primary);
    width: 30px;
    border-radius: 999px;
}

.page-element-story-carousel--article .page-carousel-card {
    gap: 0;
}

/* Red brand border on every thumb in the photo carousel. Using a
 * positioned pseudo-element (not box-shadow:inset) because the inner
 * <img> has width/height:100% + object-fit:cover, which paints over
 * any inset shadow on the thumb's background — so the inset path
 * looks invisible. The ::after layer renders ABOVE the image and
 * inherits the parent's border-radius, so the border follows the
 * rounded corners cleanly. */
.page-element-story-carousel--article .page-carousel-thumb::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 1px solid var(--md-sys-color-primary);
    border-radius: inherit;
    pointer-events: none;
}

.page-element-story-carousel--article .page-carousel-card:first-child .page-carousel-thumb {
    height: 162.5px;
}

.page-element-story-carousel--article .page-carousel-card:not(:first-child) {
    flex-basis: 162.5px;
}
.page-element-story-carousel--article .page-carousel-card:not(:first-child) .page-carousel-thumb {
    aspect-ratio: 1 / 1;
}

/* ----- feature_grid element -----
 * Mixed-layout card stack: one full-width hero with a round photo + content
 * row, then a 2-up grid of icon-chip cards. Heroes can re-appear later in
 * the items[] list to break the grid; the Twig partial opens/closes the
 * .page-feature-cards wrapper around runs of "card" layouts.
 *
 * Cards have a light-surface background so they elevate against the
 * operator's (often dark) landing background; this isn't a token override
 * because we want the visual contrast regardless of theme. */
.page-element-feature-grid {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 4px 0 0;
}

/* The 2-up grid that wraps consecutive "card" layouts. */
.page-feature-cards {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}

/* Items with width: "full" span both grid columns while keeping the
   card visual treatment (vs. "hero" which has its own photo-row look). */
.page-feature-card--full {
    grid-column: 1 / -1;
}

/* Common card surface — applied to both .page-feature-hero and .page-feature-card.
   Background + text read operator-controlled tokens: --app-feature-card-bg is
   the per-tenant default (ThemeExtension, set in /operator/appearance); an
   individual grid item overrides it inline via feature_card_style(). The
   #ffffff / #1a1a1a fallbacks keep the historical look when no token is set. */
.page-feature-hero,
.page-feature-card {
    position: relative;
    background: var(--app-feature-card-bg, #ffffff);
    color: var(--app-feature-card-on, #1a1a1a);
    border-radius: 18px;
    text-decoration: none;
    overflow: hidden;
    /* Slight inner pad — hero has a different padding ratio below. */
    padding: 14px;
    display: block;
}

/* ----- hero card: photo | text | arrow row ----- */
.page-feature-hero {
    display: grid;
    grid-template-columns: 96px 1fr auto;
    align-items: center;
    gap: 14px;
    padding: 14px 16px;
}

.page-feature-hero-media {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--md-sys-color-primary);
    display: grid;
    place-items: center;
    flex-shrink: 0;
}

.page-feature-hero-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.page-feature-hero-media .material-symbols-outlined {
    font-size: 48px;
    color: var(--md-sys-color-on-primary);
}

.page-feature-hero-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* ----- "card" layout: icon, kicker, title, subtitle, arrow ----- */
.page-feature-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

.page-feature-card-icon {
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary, #fff);
    display: grid;
    place-items: center;
    margin-bottom: 12px;
    flex-shrink: 0;
}

.page-feature-card-icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    padding: 6px;
    box-sizing: border-box;
}

.page-feature-card-icon .material-symbols-outlined {
    font-size: 24px;
    font-variation-settings: 'FILL' 1;
}

/* ----- shared typography for hero + card ----- */
.page-feature-kicker {
    color: var(--app-feature-kicker-color, var(--app-feature-kicker-default, var(--md-sys-color-primary)));
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    line-height: 1.2;
}

.page-feature-title {
    font-size: 22px;
    font-weight: 800;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--app-feature-title-color, var(--app-feature-title-default, var(--app-feature-card-on, #1a1a1a)));
    line-height: 1.1;
    word-break: break-word;
}

/* Hero title can run a bit larger / wider since it has the photo-anchored row. */
.page-feature-hero .page-feature-title {
    font-size: 22px;
}

.page-feature-card .page-feature-title {
    font-size: 18px;
    margin-top: 2px;
}

.page-feature-subtitle {
    color: var(--app-feature-subtitle-color, var(--app-feature-subtitle-default, var(--app-feature-card-on-muted, #6b6b6b)));
    font-size: 14px;
    line-height: 1.3;
    margin-top: 2px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
}

/* ----- arrow chip: brand-outlined circle with chevron ----- */
.page-feature-arrow {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 2px solid var(--md-sys-color-primary);
    color: var(--md-sys-color-primary);
    display: grid;
    place-items: center;
    flex-shrink: 0;
}

.page-feature-arrow .material-symbols-outlined {
    font-size: 20px;
}

/* Card layout pins the arrow at bottom-right of the tile; hero leaves it
 * inline in its grid column. */
.page-feature-card .page-feature-arrow {
    position: absolute;
    right: 14px;
    bottom: 14px;
}

/* The chevron chip is absolutely positioned (above) so it doesn't take part
 * in normal flow — long titles/subtitles would wrap right under it and bleed
 * through the arrow. Only *linked* cards render the chevron (they're an <a>;
 * static cards are a <div>), so reserve the right inset on linked cards only.
 * 48px = 34px arrow + 14px card pad. */
a.page-feature-card .page-feature-title,
a.page-feature-card .page-feature-kicker,
a.page-feature-card .page-feature-subtitle {
    padding-right: 48px;
}

/* ----- badge: dark pill in top-right corner ----- */
.page-feature-badge {
    position: absolute;
    top: 14px;
    right: 14px;
    background: #1a1a1a;
    color: #ffffff;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 1px;
    padding: 4px 5px;
    border-radius: 6px;
    line-height: 1;
    text-transform: uppercase;
}

/* ============================================================== */
/* Splash element — full-bleed intro screen                        */
/* ============================================================== */

/* When the body carries .splash-body, the operator's brand-color fills
 * the entire viewport (no card surface visible). Set on .landing-body
 * by page.html.twig only when the page contains a splash element. */
body.splash-body {
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
}

/* iOS Safari fix: while the OVERLAY splash covers the landing, paint the
 * body brand-primary. A `position: fixed; inset: 0` element measures the
 * initial small viewport (URL-bar visible) and does NOT resize when Safari
 * later hides the URL-bar — a strip at the bottom exposes the layer
 * behind. Matching the body to the overlay background makes that strip
 * invisible. Toggled from splash_controller.js on connect/disconnect. */
body.has-splash-overlay {
    background: var(--md-sys-color-primary);
}

/* The splash takes the full viewport — no max-width cap (unlike .landing
 * which constrains to 480px for phone-sized columns). Content centers
 * vertically + horizontally; on tall viewports there's extra breathing
 * room above the progress bar. */
.page-splash {
    min-height: 100vh;
    /* iOS 26 Safari shrinks the URL-bar area, so 100vh is smaller than
       the real viewport at times. 100dvh tracks the live viewport,
       100lvh guarantees the largest possible height. Later values win
       in browsers that understand them. */
    min-height: 100dvh;
    min-height: 100lvh;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Extend padding under the home-indicator + iOS 26 glass toolbar
       so the progress bar / tagline never sit under system chrome. */
    padding: max(32px, env(safe-area-inset-top, 0px)) 24px max(32px, env(safe-area-inset-bottom, 0px));
    box-sizing: border-box;
    /* The splash auto-redirects on a timer and also accepts a tap-anywhere
     * skip via splash_controller#skip — the pointer cursor cues visitors
     * that the surface is interactive. */
    cursor: pointer;
}

/* Overlay mode — splash layered ON TOP of an already-loaded landing.
 * Position: fixed so it covers the viewport regardless of scroll, max
 * z-index so it sits above the bottom-nav (z:9000-ish) and any modal
 * overlays. Brand-color background filled here (rather than on body)
 * because the body class stays `landing-body` for the underlying page —
 * we can't flip body.splash-body bg without breaking the landing. */
.page-splash--overlay {
    /* On iOS 26 Safari `position: fixed` measures the safe viewport and
       still leaves a strip visible behind the glass bottom-toolbar. An
       absolute element with html + body anchored to 100lvh / fill-
       available inherits the full screen height instead. */
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    /* Explicit heights are the belt for the braces above; whichever unit
       the browser understands last, wins. */
    height: 100vh;
    height: 100dvh;
    height: 100lvh;
    height: -webkit-fill-available;
    z-index: 9999;
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    transition: opacity 350ms ease-out;
}

/* Set on the overlay by splash_controller when the timer fires. Fades
 * to transparent + pulls pointer-events off so the underlying landing
 * is interactive during the fade. The controller removes the element
 * from the DOM after the transition completes. */
.page-splash--overlay.is-leaving {
    opacity: 0;
    pointer-events: none;
}

.page-splash-stage {
    width: 100%;
    max-width: 480px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
}

/* While the splash images are still downloading, splash_controller keeps the
 * `is-loading` class on. Hide the whole stage (black/brand screen only) and
 * freeze the progress fill at scaleX(0) so the bar doesn't start — nor the
 * redirect timer fire — before the artwork has painted. The controller drops
 * `is-loading` once every <img> has loaded, revealing the stage and starting
 * the bar from zero. */
.page-splash.is-loading .page-splash-stage {
    visibility: hidden;
}

.page-splash.is-loading .page-splash-progress-fill {
    animation-play-state: paused;
}

/* Hero photo frame. Default is a square (1:1) container with no
 * background — the photo fills it edge-to-edge and any tint would just
 * read as a misaligned border. The operator opts into the circular
 * crop per splash via the `image_is_circle` config flag, which adds the
 * `--circle` modifier class below; that's where the halo background
 * lives because in circle mode the disc reads as a deliberate ring
 * around the portrait. Operator-uploaded image is object-fit: cover so
 * arbitrary photo aspect ratios crop nicely into the frame. */
.page-splash-photo {
    width: 60vw;
    max-width: 350px;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    display: grid;
    place-items: center;
    /* Margin pushes the photo down a bit from vertical center so the
     * wordmark + tagline below feel anchored to the lower half — matches
     * the mockup's composition. */
    margin-top: -40px;
}

/* Circle modifier — applied when `element.config.image_is_circle` is
 * true. Rounds the frame into a full circle and adds the translucent
 * on-primary halo so the cropped portrait sits on a tinted disc. */
.page-splash-photo--circle {
    border-radius: 50%;
    background: color-mix(in srgb, var(--md-sys-color-on-primary) 18%, transparent);
}

.page-splash-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.page-splash-photo .material-symbols-outlined {
    font-size: 96px;
    color: var(--md-sys-color-on-primary);
    opacity: 0.6;
}

/* Wordmark uses the operator's custom font (set via theme_css emits
 * --app-font-family). Falls back to the UnifrakturCook stack that the
 * normal landing header uses, so an operator without a custom font still
 * gets a "logo-looking" wordmark. */
.page-splash-wordmark {
    font-family: var(--app-font-family, 'UnifrakturCook', 'Times New Roman', serif);
    font-size: 64px;
    line-height: 1;
    font-weight: 700;
    color: var(--md-sys-color-on-primary);
    letter-spacing: 0.5px;
    text-align: center;
}

/* Operator-uploaded wordmark art rendered in place of the text wordmark
 * when config.wordmark_image_path is set. Renders at the asset's natural
 * pixel size when it fits the splash stage; oversized images scale down
 * to the container width with the aspect ratio preserved (no crop).
 * Horizontal centering comes from the parent .page-splash-stage's
 * `align-items: center`; margin-top mirrors the text-wordmark variant
 * so layouts stay consistent when the operator toggles between text
 * and image. */
.page-splash-wordmark-image {
    display: block;
    max-width: 100%;
    height: auto;
    margin-top: 12px;
}

.page-splash-tagline {
    margin: 0;
    font-size: 18px;
    font-weight: 500;
    text-transform: uppercase;
    color: var(--md-sys-color-on-primary);
    text-align: center;
    line-height: 1.3;
}

/* Progress bar — an animated fill on a faded track. Inert for now
 * (Phase 2b can hook this up to an auto-redirect timer).
 *
 * The fill color resolves to --page-splash-progress-color when the
 * splash template emits it (set per-element via config.progress_color),
 * falling back to the M3 on-primary token so legacy splashes look
 * identical to before. The track is intentionally decoupled from the
 * fill — fixed at white-20%-opacity regardless of the chosen fill
 * color, so the bar reads as "filled portion vs. faint backdrop"
 * rather than two shades of the same hue. Operator-configurable track
 * color would need a CSS-color text input (native <input type=color>
 * doesn't support alpha); not worth the form complexity for now. */
.page-splash-progress {
    width: 60%;
    max-width: 280px;
    height: 2.5px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.2);
    overflow: hidden;
    margin-top: 16px;
}

.page-splash-progress-fill {
    display: block;
    height: 100%;
    width: 100%;
    background: var(--page-splash-progress-color, var(--md-sys-color-on-primary));
    transform-origin: left center;
    /* --page-splash-duration is set inline by splash_controller.js from
     * the operator-configured duration_ms (or its 2500ms default), so the
     * bar fill animation and the auto-redirect timer stay aligned. */
    animation: page-splash-fill var(--page-splash-duration, 2500ms) ease-out forwards;
}

@keyframes page-splash-fill {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
    .page-splash-progress-fill {
        animation: none;
        transform: scaleX(1);
    }
}

/* ============================================================== */
/* In-app browser shell — /x/{id} iframe wrapper                    */
/* ============================================================== */

/* Lock the body so the iframe owns the viewport. Without overflow:hidden
 * on the body the page can still scroll behind the fixed shell on mobile
 * browsers, which feels broken. */
body.iframe-shell-body {
    margin: 0;
    overflow: hidden;
    /* Operator's brand background fills the small slivers between the
     * iframe and the bottom bar before the iframe paints. */
    background: var(--app-landing-bg, var(--md-sys-color-surface-container-lowest));
}

.iframe-shell {
    position: fixed;
    inset: 0;
    display: flex;
    flex-direction: column;
    /* Match the bottom-nav footer: full-bleed on phones, capped + centered at
     * 480px on wider viewports. No horizontal padding here — the frame goes
     * edge-to-edge of this column (the header carries its own inset below).
     * The bottom reserve clears the fixed bottom-nav so the frame fills the
     * space right up to the footer without hiding behind it. */
    max-width: 480px;
    margin: 0 auto;
    padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
}

/* The fixed footer keeps its full-width positioning (don't make it in-flow —
 * its max-width:480 + margin:auto then shrink it into a narrower box, leaving
 * brand-bg slivers beside the buttons). A couple extra px of top padding grow
 * the white bar up just enough to close the thin brand-bg gap above it without
 * touching the landing / module-detail copies of this nav. */
.iframe-shell .landing-bottomnav {
    padding-top: 8px;
}

/* Brand header sits unstyled on the operator background (same as the landing
 * page top). Its own 16px side inset keeps the logo/profile off the edge while
 * the frame below stays full-bleed; flex-shrink:0 keeps it from collapsing. */
.iframe-shell-header {
    flex-shrink: 0;
    padding-left: 16px;
    padding-right: 16px;
}

.iframe-shell-frame {
    /* Fill all space above the bottom bar. flex:1 + min-height:0 is the
     * standard "flexbox child can shrink below its content" recipe — without
     * min-height:0 the iframe insists on its intrinsic height and the
     * bottom bar gets pushed off-screen. */
    flex: 1;
    min-height: 0;
    width: 100%;
    border: 0;
    /* White background so the brief moment before the iframe paints isn't
     * a flash of the operator's dark brand color. */
    background: #ffffff;
}

.iframe-shell-bar {
    /* Sticky bottom-bar — same height profile + surface tokens as the
     * standard landing-bottomnav so visitors feel a consistent app shell
     * and the bar reads correctly regardless of the operator's theme.
     *
     * Both surface-container-lowest and on-surface-variant deliberately
     * stay at their M3 light defaults — they're NOT overridden by
     * theme_css. That keeps the bar light + readable on dark-themed
     * operators (Redo: black landing, white text_color → using on-surface
     * here would have produced white-on-white invisible text).
     *
     * Only the open-in-new-tab chip currently lives in the bar (back
     * chevron removed per operator preference); flex-end pins it to the
     * right. If you re-add the back chip later, switch to space-between. */
    flex-shrink: 0;
    min-height: 64px;
    background: var(--md-sys-color-surface-container-lowest);
    border-top: 1px solid var(--md-sys-color-outline-variant);
    color: var(--md-sys-color-on-surface-variant);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 12px;
    padding: 8px 12px;
    /* Safe-area inset for notched / dynamic-island devices — keeps the
     * bar above the home-indicator bar on iOS. */
    padding-bottom: max(8px, env(safe-area-inset-bottom));
}

.iframe-shell-back,
.iframe-shell-external {
    /* Default state uses the operator's brand primary — these chips feel
     * like the dominant action in the bar (especially "Back"), so the
     * brand color earns its place here. The bar itself stays on neutral
     * surface tokens so the brand-tinted chips read clearly against it. */
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--md-sys-color-primary);
    text-decoration: none;
    padding: 10px 14px;
    border-radius: var(--md-sys-shape-corner-full);
    overflow: hidden;
    transition: background var(--md-sys-motion-duration-short2, 150ms);
}

/* Hover / keyboard-focus tint — M3 state-layer pattern, 12% primary mixed
 * with the transparent surface. md-ripple still fires on press for the
 * brief impact feedback; this rule covers the resting hover state. */
.iframe-shell-back:hover,
.iframe-shell-back:focus-visible,
.iframe-shell-external:hover,
.iframe-shell-external:focus-visible {
    background: color-mix(in srgb, var(--md-sys-color-primary) 12%, transparent);
}

.iframe-shell-back .material-symbols-outlined {
    font-size: 22px;
}

.iframe-shell-back-label {
    font-size: 15px;
    font-weight: 600;
}

.iframe-shell-external {
    /* Icon-only chip; smaller padding than the back button keeps the
     * proportions balanced (back has icon + label, external has icon only). */
    padding: 10px;
}

.iframe-shell-external .material-symbols-outlined {
    font-size: 22px;
}

/* ============================================================
 * Voucher redeem (daily meal) — button + proof-screen overlay
 * Element type: TYPE_VOUCHER_REDEEM. Renders on operator-approved
 * weekdays only; the proof overlay is built dynamically by the
 * voucher-redeem Stimulus controller from a hidden <template>.
 * ============================================================ */

.voucher-redeem {
    margin: 24px 16px;
    display: flex;
    justify-content: center;
}

.voucher-redeem-button {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 14px 28px;
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    border: none;
    border-radius: 999px;
    font: inherit;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: 0.2px;
    cursor: pointer;
    overflow: hidden;
    /* M3 elevation level 1 */
    box-shadow: 0 1px 3px 1px rgba(0, 0, 0, 0.15), 0 1px 2px 0 rgba(0, 0, 0, 0.3);
    transition: transform 120ms ease, box-shadow 120ms ease;
}

.voucher-redeem-button:hover {
    transform: translateY(-1px);
}

.voucher-redeem-button:active {
    transform: translateY(0);
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
}

.voucher-redeem-button[disabled] {
    opacity: 0.6;
    cursor: progress;
}

/* "Disabled" off-day visual — distinct from the in-flight [disabled]
 * state above. The button still receives clicks (the JS handler shows
 * a popup with the operator's allowed weekdays) so we use a class, not
 * the `disabled` attribute. Keep the same shape + ripple footprint;
 * just desaturate to surface-variant so the affordance reads as
 * "present but not actionable today". */
.voucher-redeem-button--disabled {
    background: var(--md-sys-color-surface-variant);
    color: var(--md-sys-color-on-surface-variant);
    box-shadow: none;
    /* cursor: not-allowed; */
}

.voucher-redeem-button--disabled:hover,
.voucher-redeem-button--disabled:active {
    transform: none;
    box-shadow: none;
}

.voucher-redeem-button .material-symbols-outlined {
    font-size: 22px;
}

/* ---- Proof overlay ---- */

.voucher-proof {
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 32px 24px;
    background: var(--md-sys-color-primary);
    color: var(--md-sys-color-on-primary);
    opacity: 0;
    transition: opacity 250ms ease;
    overflow: hidden;
    /* Tap on backdrop does NOT close — staff must use the explicit X.
     * Accidental dismissal during the proof is the worst possible UX. */
}

.voucher-proof.is-entering {
    opacity: 1;
}

.voucher-proof.is-leaving {
    opacity: 0;
}

.voucher-proof-photo,
.voucher-proof-photo-initial {
    position: relative;
    z-index: 2;
    width: min(60vw, 240px);
    height: min(60vw, 240px);
    border-radius: 50%;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.12);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
    display: flex;
    align-items: center;
    justify-content: center;
}

.voucher-proof-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.voucher-proof-photo-initial {
    font-size: 96px;
    font-weight: 600;
}

.voucher-proof-name {
    position: relative;
    z-index: 2;
    margin-top: 20px;
    font-size: 22px;
    font-weight: 600;
    text-align: center;
}

.voucher-proof-clock {
    position: relative;
    z-index: 2;
    margin-top: 14px;
    font-variant-numeric: tabular-nums;
    font-size: 28px;
    font-weight: 500;
    letter-spacing: 0.5px;
}

.voucher-proof-confirmation {
    position: relative;
    z-index: 2;
    margin: 18px 8px 4px;
    max-width: 420px;
    font-size: 18px;
    text-align: center;
    line-height: 1.4;
}

.voucher-proof-branch {
    position: relative;
    z-index: 2;
    margin: 6px 8px 0;
    font-size: 14px;
    opacity: 0.75;
    text-align: center;
}

.voucher-proof-code {
    position: relative;
    z-index: 2;
    margin-top: 14px;
    padding: 6px 12px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.22);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: 2px;
}

/* ---- Countdown ---- */

.voucher-proof-countdown {
    position: absolute;
    left: 24px;
    right: 24px;
    bottom: 32px;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 12px;
}

.voucher-proof-countdown-bar {
    flex: 1;
    height: 4px;
    background: currentColor;
    border-radius: 2px;
    /* Width is animated by JS via requestAnimationFrame. */
    transition: width 100ms linear;
    width: 100%;
}

.voucher-proof-countdown-text {
    font-variant-numeric: tabular-nums;
    font-size: 14px;
    font-weight: 600;
    min-width: 4ch;
    text-align: right;
}

/* ---- Close button ---- */

.voucher-proof-close {
    position: absolute;
    top: 16px;
    right: 16px;
    z-index: 3;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: none;
    background: rgba(0, 0, 0, 0.25);
    color: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.voucher-proof-close .material-symbols-outlined {
    font-size: 24px;
}

/* ---- Watermark ---- */

/*
 * Operator-styled drifting watermark. The text is repeated into a single
 * <div> (controller appends 16 copies); we let it wrap naturally and then
 * tile it visually by scaling, rotating, and animating the X position.
 * The diagonal angle + slow drift make it hostile to screenshot/video
 * capture without harming legibility of the rest of the overlay.
 */
.voucher-proof-watermark {
    position: absolute;
    inset: -25% -25%;
    z-index: 1;
    pointer-events: none;
    color: var(--voucher-watermark-color, #FFFFFF);
    opacity: var(--voucher-watermark-opacity, 0.25);
    font-size: clamp(48px, 12vw, 96px);
    font-weight: 800;
    letter-spacing: 4px;
    line-height: 1.6;
    overflow-wrap: break-word;
    text-align: center;
    transform: rotate(-20deg);
    animation: voucher-watermark-drift 9s linear infinite;
    /* Stack copies vertically by line height + horizontal wrap; the drift
     * animation walks both axes so a single frame never captures the
     * full pattern. */
}

@keyframes voucher-watermark-drift {
    0% {
        transform: rotate(-20deg) translate(-2%, -1%);
    }
    50% {
        transform: rotate(-20deg) translate(2%, 1%);
    }
    100% {
        transform: rotate(-20deg) translate(-2%, -1%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .voucher-proof,
    .voucher-proof.is-entering,
    .voucher-proof.is-leaving {
        transition: none;
    }
    .voucher-proof-watermark {
        animation: none;
    }
}
