  :root {
    /* Theme-driven — overwritten by theme.js applyTheme() (reads the
       active palette from Postgres, shared with the admin panel). These are
       fallback defaults (the Awqōt palette) so the page looks right even
       before that async fetch resolves. Nothing below this block
       references a raw hex color. */
    --color-primary: #0B4A44;
    --color-secondary: #FFFFFF;
    --color-accent: #C89B3C;
    --theme-transition-ms: 400ms;

    /* Role convention: primary = text/ink, secondary = surface/background
       (see theme.js). Card/divider shades blend toward each palette's own
       primary rather than a fixed white/black, so this works correctly
       whether secondary happens to be light (12 of 13 palettes) or dark
       (Kaabah). */
    --bg: var(--color-secondary);
    --panel-bg: color-mix(in srgb, var(--color-secondary) 92%, var(--color-primary) 8%);
    --text-primary: var(--color-primary);
    --text-muted: color-mix(in srgb, var(--color-primary) 75%, var(--color-secondary) 25%);
    --accent: var(--color-accent);
    --divider: color-mix(in srgb, var(--color-secondary) 80%, var(--color-primary) 20%);

    /* Amiri/Source Sans 3 both loaded as webfonts (see each page's <head>)
       so text renders identically on every OS instead of falling back to
       whatever the system happens to have installed — the system stacks
       after them are last-resort fallbacks only, for if a font fails to
       load. Amiri only ships in 400/700 — no 600 — so every heading rule
       below requests 700, not 600. */
    --font-display: "Amiri", Georgia, serif;
    --font-body: "Source Sans 3", -apple-system, "Segoe UI", sans-serif;
    --font-arabic: "Amiri Quran", "Scheherazade New", serif;
  }

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    transition: background-color var(--theme-transition-ms) ease,
                color var(--theme-transition-ms) ease,
                border-color var(--theme-transition-ms) ease;
  }

  html, body {
    width: 100dvw;
    height: 100dvh;
    overflow: hidden;
    background: var(--bg);
    font-family: var(--font-body);
    color: var(--text-primary);
  }

  /* dvw/dvh (not vw/vh) — .stage is what fullscreen-toggle.js puts into
     the Fullscreen API. Static vw/vh are computed against the layout
     viewport at layout time and don't reliably re-track it; entering real
     OS fullscreen removes browser chrome (and sometimes the OS taskbar)
     after that, and html/body's own overflow:hidden above suppresses the
     resize/scroll signals that would otherwise force a recalculation. The
     visible symptom was a thin gap at the top of .slide-zone showing the
     browser's black fullscreen backdrop through — .slide-zone has no
     background of its own (the logo dangles over it with nothing painted
     behind), while .panel's opaque background masked the same-sized gap
     on its side. dvw/dvh are spec'd to live-track the actual current
     viewport through exactly this kind of chrome show/hide transition. */
  .stage {
    display: flex;
    width: 100dvw;
    height: 100dvh;
  }

  /* Belt-and-suspenders alongside the dvw/dvh switch above: confirmed this
     reproduces on every fullscreen-toggle.js-triggered click, not just an
     occasional race, so paint .stage's own background explicitly while
     fullscreen rather than relying solely on sizing being exactly right.
     Without this, any residual gap between .stage's box and the real
     screen falls through to the Fullscreen API's own ::backdrop, whose
     default is black — with this, the same gap reads as the kiosk's own
     background instead of a jarring black flash. */
  .stage:fullscreen,
  .stage:-webkit-full-screen {
    background: var(--bg);
  }

  /* LEFT — 75% zone: PDF slide carousel, with the mosque logo dangling
     over its top-left corner (see kiosk-logo.js) — no background bar, no
     tagline (removed 2026-07-28 along with .slide-header/.mosque-label).
     overflow:visible (default, stated explicitly here) is what lets
     .kiosk-logo's negative margin below dangle over .slides without being
     clipped at this container's own edge. */
  .slide-zone {
    flex: 3;
    height: 100%;
    display: flex;
    flex-direction: column;
    overflow: visible;
    /* Containing block for .fullscreen-toggle-btn below (anchors it to
       this zone specifically, not the whole viewport) and for
       .prayer-override-card in both its modes (see prayer-override-card.js
       — sidepanel mode deliberately targets this zone too, not .panel, so
       the clock/timing panel keeps running untouched alongside the card). */
    position: relative;
  }

  /* Height-capped, width auto — preserves the logo's own aspect ratio (no
     distortion) regardless of source image dimensions. align-self so it
     doesn't stretch to the column's full width. Negative bottom margin
     (matching its own rendered height, since max-height forces a
     consistent 65px) cancels its layout contribution so .slides starts
     right under it rather than being pushed down by the logo's full
     height — only the small top/left margin's inset remains. z-index
     beats .slides' own position:relative (z-index:auto) so the logo
     paints on top of whatever slide is showing, not behind it. */
  .kiosk-logo {
    max-height: 65px;
    width: auto;
    flex-shrink: 0;
    align-self: flex-start;
    object-fit: contain;
    position: relative;
    z-index: 2;
    margin: 16px 0 -65px 16px;
  }

  .kiosk-logo.hidden {
    display: none;
  }

  /* PDF announcement slide carousel (see slide-carousel.js). Same
     --panel-bg token as the right-hand timing panel, so the
     letterbox/pillarbox bars around whatever's currently loaded read as
     the same panel rather than a black gap. */
  .slides {
    flex: 1;
    background: var(--panel-bg);
    position: relative;
    /* Suppresses native double-tap-zoom on this element specifically, so
       slide-lightbox.js's own double-tap detector can fire reliably
       instead of racing the browser's own gesture recognizer for it (see
       that file's header comment). Doesn't affect the swipe handler in
       slide-carousel.js — that reads raw touch coordinates directly,
       untouched by touch-action either way. Deliberately not applied
       inside the lightbox itself — native pinch-zoom/pan needs to work
       freely there, per spec. */
    touch-action: manipulation;
  }

  .slide-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain; /* preserves PDF aspect ratio — letterbox/pillarbox, never stretch */
  }

  .slide-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    font-size: 1.2rem;
  }

  /* Floating fullscreen toggle, bottom-right of the slide area. Background
     is theme-driven (--text-primary, the palette's primary color) rather
     than the fixed rgba() this started as — text stays a literal #fff
     since that's not itself something a palette provides a guaranteed
     opposite for. Starts hidden — fullscreen-toggle.js only reveals it
     once it's confirmed the Fullscreen API actually exists (notably absent
     on older iOS Safari for non-video elements), rather than showing a
     control that would silently do nothing on tap. */
  .fullscreen-toggle-btn {
    position: absolute;
    bottom: 16px;
    right: 16px;
    z-index: 5;
    padding: 8px 14px;
    border: none;
    border-radius: 999px;
    background: var(--text-primary);
    color: #fff;
    font-family: var(--font-body);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.3s ease, opacity 0.3s ease;
  }

  .fullscreen-toggle-btn:hover {
    background: color-mix(in srgb, var(--text-primary) 85%, black);
  }

  .fullscreen-toggle-btn.hidden {
    display: none;
  }

  /* Guaranteed-reliable fallback opener for the same lightbox the
     double-tap/dblclick gesture opens (see slide-lightbox.js) — bottom-
     left, opposite .fullscreen-toggle-btn (bottom-right) and clear of
     .kiosk-logo (top-left), so none of the three overlap. Same pill
     treatment/z-index tier as .fullscreen-toggle-btn, icon-only (fixed
     square) rather than that button's text-label sizing. Always visible,
     no auto-hide — unlike the fullscreen button this is meant to be the
     dependable path in regardless of gesture reliability, not something
     to tuck away. */
  .slide-enlarge-btn {
    position: absolute;
    bottom: 16px;
    left: 16px;
    z-index: 5;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 999px;
    background: var(--text-primary);
    color: #fff;
    cursor: pointer;
  }

  .slide-enlarge-btn:hover {
    background: color-mix(in srgb, var(--text-primary) 85%, black);
  }

  .slide-enlarge-btn svg {
    width: 20px;
    height: 20px;
  }

  /* Auto-hidden 3s into fullscreen (see fullscreen-toggle.js) — slides
     down out of view rather than just fading, so it reads as "tucked
     away" instead of "broken/disappeared". pointer-events:none once
     hidden so it can't intercept taps meant for whatever's behind it,
     even before the transform finishes animating off-box. */
  .fullscreen-toggle-btn.auto-hidden {
    transform: translateY(150%);
    opacity: 0;
    pointer-events: none;
  }

  /* Prayer Time Override Card (see prayer-override-card.js) — always lives
     inside .slide-zone (appendChild there is a no-op move if it's already
     the parent), for both modes: fullscreen because .slide-zone is the
     (now-expanded) whole stage, sidepanel because the override is only
     meant to take over the slide/carousel area while .panel's clock/
     timetable/quote-feed keep running untouched and visible beside it —
     there is no .panel target at all. One rule covers both as an
     absolute-fill overlay of whichever width .slide-zone currently has.
     z-index above .kiosk-logo (2) and .fullscreen-toggle-btn (5) so it
     fully covers both. opacity/transition duration matches FADE_MS in
     prayer-override-card.js and reuses the same cross-fade idiom as
     .quote-content/.is-hidden — hardcoded, no other transition presets.
     pointer-events follows visibility since this is a passive display
     card, nothing to interact with either way. */
  .prayer-override-card {
    position: absolute;
    inset: 0;
    z-index: 20;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 16px;
    background: var(--panel-bg);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.9s ease;
  }

  .prayer-override-card.is-visible {
    opacity: 1;
    pointer-events: auto;
  }

  .prayer-override-card.hidden {
    display: none;
  }

  .prayer-override-card-name,
  .prayer-override-card-time {
    font-family: var(--font-display);
    line-height: 1;
    text-align: center;
  }

  .prayer-override-card-name {
    font-weight: 700;
    color: var(--text-primary);
  }

  .prayer-override-card-time {
    font-weight: 600;
    color: var(--accent);
    font-variant-numeric: tabular-nums;
  }

  /* Platform Notice (see platform-notice.js) — z-index 21, one above
     .prayer-override-card's 20 (previously the highest used within
     .slide-zone's own stacking context, see .slide-lightbox's comment
     above): a platform-wide maintenance notice should visually win over
     either of the other .slide-zone takeovers in the unlikely case both
     are active at once, not the other way around. Simple opaque cover,
     no cross-fade — this is an infrequent, admin-triggered state change,
     not a frequent animated transition like the other two. */
  .platform-notice-overlay {
    position: absolute;
    inset: 0;
    z-index: 21;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 5%;
    text-align: center;
    background: var(--panel-bg);
  }

  .platform-notice-overlay.hidden {
    display: none;
  }

  .platform-notice-overlay-text {
    font-family: var(--font-display);
    font-weight: 600;
    color: var(--text-primary);
    font-size: clamp(24px, 4vw, 64px);
    line-height: 1.3;
    white-space: pre-wrap;
  }

  /* Fullscreen mode fills the whole (now-expanded) slide-zone (~100% of
     the viewport), so it can read at a distance the same way the kiosk's
     own .clock does. Sidepanel mode fills the same .slide-zone element,
     but .panel is still visible beside it there (flex:3 of a 3:1 split,
     ~75% of the viewport, not 100%) — sized down from the fullscreen tier
     roughly in proportion to that narrower box, rather than reusing
     identical vw-based values that would overflow it. */
  .prayer-override-card--fullscreen .prayer-override-card-name {
    font-size: clamp(48px, 10vw, 200px);
  }

  .prayer-override-card--fullscreen .prayer-override-card-time {
    font-size: clamp(32px, 6vw, 120px);
  }

  .prayer-override-card--sidepanel .prayer-override-card-name {
    font-size: clamp(40px, 8vw, 160px);
  }

  .prayer-override-card--sidepanel .prayer-override-card-time {
    font-size: clamp(28px, 5vw, 100px);
  }

  /* Double-tap/double-click-to-enlarge lightbox (see slide-lightbox.js).
     Direct child of .stage (not .slide-zone) so this fixed inset:0
     overlay covers .panel too, in every layout mode. z-index 50 — above
     everything else in .stage, including .prayer-override-card (20), the
     highest previously used here. Near-opaque dark backdrop regardless of
     the active theme palette — standard convention for an image viewer,
     keeps focus on the slide rather than competing with it. Same
     opacity-only cross-fade idiom as .prayer-override-card above
     (0.9s ease, .hidden for display:none, .is-visible for the fade own
     state) for visual consistency with the other overlay already doing
     this same job on this same element tree. */
  .slide-lightbox {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.92);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.9s ease;
  }

  .slide-lightbox.is-visible {
    opacity: 1;
    pointer-events: auto;
  }

  .slide-lightbox.hidden {
    display: none;
  }

  /* Live Session takeover (see js/live-session.js) — deliberately NOT the
     .slide-lightbox pattern above (position:fixed against the whole
     viewport, covering .panel too): that's correct for a one-off,
     user-triggered zoom, but a live session is the kiosk's normal ongoing
     state, live session or not — the right-side timing panel (.panel)
     must stay visible and keep updating throughout, exactly as it does
     during normal Kiosk Mode rotation. Sibling of #slidePanel within
     .slide-zone instead (which already has position:relative — same
     containing block #fullscreenToggleBtn/#prayerOverrideCard above
     already use), absolutely positioned to fill *that* container, not the
     viewport. No explicit z-index: painting after #slidePanel in DOM
     order is enough to sit above the carousel's own canvas, while staying
     below .kiosk-logo's explicit z-index:2 — the logo keeps dangling over
     live-session content exactly as it already does over normal slides,
     nothing special-cased for it here. No camera compositing yet
     (deferred past Sept 9) — just the selected deck's page.
     background: var(--panel-bg), NOT a solid black backdrop like the
     lightbox above — that black was copied from the lightbox's own
     "dim everything else, focus on this" treatment, appropriate for a
     temporary zoom viewer but wrong here: a live session should look like
     a normal, permanent part of Kiosk Mode, not something layered on top
     of it. .slides (the carousel's own panel, right below) already uses
     this same token specifically so its own letterbox/pillarbox bars read
     as "the panel," not a black gap — this needs the exact same
     treatment for the same reason, or a deck page with different
     proportions than the panel would visibly look like it's showing
     through a dark scrim instead of just being the panel's content. */
  .live-session-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--panel-bg);
  }

  .live-session-overlay.hidden {
    display: none;
  }

  .live-session-canvas {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain; /* same aspect-ratio-preserving convention as .slide-canvas */
  }

  /* .khutbah-overlay/.khutbah-background/.khutbah-subtitle-bar/
     .khutbah-subtitle-text moved to css/khutbah-overlay.css (linked
     below) — shared verbatim with admin/index.html's own live preview
     (see renderKhutbahOverlayPreview() in admin.js) so the two can never
     visually drift apart. See that file's own header comment for why a
     small shared partial, not a wholesale @import, is what admin.css's
     own header comment already warns against.

     .khutbah-camera-video is NOT in that shared partial, deliberately —
     it briefly lived in css/admin.css instead (Prompt A+B, camera
     capture temporarily relocated off the kiosk for a remote-pipeline
     model that's since been killed outright — see js/live-session.js's
     own header comment). That relocation left the KIOSK's own copy of
     this rule missing entirely, a real bug found while correcting the
     capture location back to here: the kiosk never actually linked
     admin.css, so #khutbahCameraVideo (see index.html) rendered
     completely unstyled default <video> sizing the whole time A+B was
     live. Restored to its original home below, admin.css's copy removed
     — the admin page's own dormant camera-preview markup (kept, not
     removed, per that pass's own scope) simply has no styling for now;
     harmless, since nothing mounts a video into it anymore either. */
  .khutbah-camera-video {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Right-panel A/B/C/D decomposition — supersedes the old whole-panel
     panelPrayerContent/panelCameraSlot swap (Slide-Dominant Mode's first
     pass): only Region B (the fluid middle slot) and Region C (bottom
     third) ever swap now, independently, while A (date/clock/next-
     prayer) and D (branding) stay untouched direct children of .panel —
     see index.html's own comments on each region for the full picture.
     .panel is unchanged (still flex-direction:column) — these regions
     are just two more items in that same column, not a new layout
     mode. */

  /* Region B's alternate content (camera feed OR static image) — the
     SAME <video class="khutbah-camera-video"> element Khutbah Mode's
     full-bleed view uses, reparented into #panelBCameraMount instead of
     duplicated (see index.html's own comment there and js/live-
     session.js's mountCameraForPanelB()). Overrides the position:
     absolute/inset:0 base rule above via ordinary specificity. Sized as
     a small preview now, not flex:1-fills-the-panel like the old
     whole-panel swap — Region B shares the panel with A/C/D now, it
     doesn't replace them. 16:9 is a plain, undocumented-elsewhere
     judgment call (a natural camera-feed aspect ratio, "(small)" per
     the locked spec's own language) — worth a real visual pass once
     this renders for real, not treated as a locked number. #panelBImage
     shares this same sizing via the .panel-region-b-media class both
     elements carry, so the camera-feed and static-image cases look
     visually identical from B's own perspective. */
  .panel .khutbah-camera-video,
  .panel-region-b-media {
    position: static;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: 8px;
  }

  /* Plain hidden-class swap between #panelBDefault (timetable +
     attribution, kept together as one unit) and whichever
     .panel-region-b-media element is currently showing — mirrors the
     old panelPrayerContent/panelCameraSlot swap mechanics exactly, just
     scoped to this one region's three children instead of the whole
     panel. */
  .panel-region-b {
    display: flex;
    flex-direction: column;
  }

  #panelBDefault.hidden,
  .panel-region-b-media.hidden {
    display: none;
  }

  /* Region C's alternate content — same two-sibling hidden-class swap
     as Region B, between #quoteFeed (default) and #panelCImage (shown
     whenever any live session is active — see js/live-session.js).
     Same 16:9/cover/radius treatment as Region B's media for visual
     consistency between the two image-capable slots, same "worth a
     real visual pass" caveat.

     margin-top: auto here (not on #quoteFeed/#panelCImage individually)
     is what actually pushes Region C + Region D (.panel-powered-by,
     the next sibling after this) flush to the bottom of .panel —
     BUG FIXED: this used to live on .quote-feed itself, back when it
     was a direct child of .panel; the A/B/C/D decomposition nested it
     one level deeper in here, which left the old rule with nothing but
     this wrapper's own already-exact-fit empty space to consume (a
     no-op — .panel-region-c never stretched to fill .panel's leftover
     height in the first place, so there was no free space at that
     level for an auto margin on its *child* to absorb). Moving the
     rule up to this element — the thing that's actually .panel's direct
     flex item now — restores the original flush-bottom behavior
     regardless of which of the two children (#quoteFeed or
     #panelCImage) is currently showing, since the anchoring is on the
     wrapper, not the inner content. */
  .panel-region-c {
    display: flex;
    flex-direction: column;
    margin-top: auto;
  }

  #panelCImage {
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: 8px;
  }

  #quoteFeed.hidden,
  #panelCImage.hidden {
    display: none;
  }

  /* Reparenting target for the carousel's *existing* <canvas> (see
     slide-lightbox.js) — not a fresh render. Its .slide-canvas rule
     already sets object-fit:contain/100%/100%, so sizing this wrap to
     the available viewport (minus a margin so the image never touches
     the screen edges) is enough for the browser to scale the same
     bitmap up correctly, no extra sizing logic needed on the canvas
     itself. */
  .slide-lightbox-canvas-wrap {
    position: relative;
    width: 92vw;
    height: 92dvh;
  }

  .slide-lightbox-close {
    position: absolute;
    top: 16px;
    right: 16px;
    z-index: 1;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 999px;
    background: var(--text-primary);
    color: #fff;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
  }

  .slide-lightbox-close:hover {
    background: color-mix(in srgb, var(--text-primary) 85%, black);
  }

  /* Small and unobtrusive per spec — opposite corner from the close
     button so the two never crowd each other. */
  .slide-lightbox-paused {
    position: absolute;
    top: 16px;
    left: 16px;
    z-index: 1;
    padding: 6px 12px;
    border-radius: 999px;
    background: var(--text-primary);
    color: #fff;
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .slide-lightbox-paused.hidden {
    display: none;
  }

  /* RIGHT — timing panel, 25%. Scrolls internally on shorter viewports
     (tablet/laptop screen heights) rather than clipping the timetable —
     `html, body` still have overflow:hidden so nothing scrolls the whole
     page, only this panel scrolls itself when its content doesn't fit.
     No parent flex `gap` — spacing between sections comes from each
     section's own margin (matching the design reference's per-element
     approach) so it can be tuned per-section instead of uniformly. */
  .panel {
    flex: 1;
    height: 100%;
    background: var(--panel-bg);
    border-left: 1px solid var(--divider);
    display: flex;
    flex-direction: column;
    padding: 24px;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: thin;
    scrollbar-color: var(--divider) transparent;
    /* Query container for .date-block's single-line/stacked switch below —
       panel width, not viewport width, is what actually determines whether
       the dates fit on one line. A viewport media query calibrated for the
       kiosk's ~25%-of-viewport panel would miscalibrate for this same
       .panel class used as the marketing demo panel, which goes 100% width
       on mobile (see the @media (max-width: 860px) rule further down) —
       same viewport, very different available width. */
    container-type: inline-size;
    container-name: panel;
  }

  .panel::-webkit-scrollbar { width: 6px; }
  .panel::-webkit-scrollbar-track { background: transparent; }
  .panel::-webkit-scrollbar-thumb { background: var(--divider); border-radius: 3px; }

  /* Mobile kiosk view — jemaah browsing this same URL on their own phone,
     away from the physical mounted display (see kiosk-logo.js/
     slide-carousel.js swipeEnabled). Hides only the *kiosk's* timing panel
     (`.stage .panel`, not the shared `.panel` class the landing/about/
     features demo aside also uses — that one is untouched, out of scope
     here) so the slide carousel gets the full screen; .slide-zone (flex:3)
     naturally expands to fill it since it's then the only flex child left
     in .stage, no separate width override needed. Same 860px breakpoint
     as the landing nav's own mobile toggle below, for consistency. */
  @media (max-width: 860px) {
    .stage .panel {
      display: none;
    }

    /* Desktop/mounted-kiosk-only now — see .slide-enlarge-btn below for
       the mobile-only counterpart. The lightbox both buttons open (see
       slide-lightbox.js) already covers the "see the slide bigger" need
       on mobile via double-tap + the enlarge button; the browser
       Fullscreen API this button drives is the desktop-relevant control
       (hiding chrome around a mounted display), not really meaningful on
       a phone browsing the same URL. */
    .fullscreen-toggle-btn {
      display: none;
    }
  }

  /* Mirror of the block above — mobile-only. Same 860px breakpoint,
     opposite direction: this is the mobile-relevant control (see its own
     rule/comment further up for why), so it's hidden past the point
     .panel above stops being hidden. */
  @media (min-width: 861px) {
    .slide-enlarge-btn {
      display: none;
    }
  }

  /* Same effect as the mobile rule above, via a JS-toggled class instead
     of a media query — see layout-state.js. Two independent reasons can
     set this (the persistent clock_panel_hidden admin toggle, and the
     Prayer Time Override Card's forced-fullscreen mode), OR'd together
     there, so either one alone flipping off doesn't collapse the layout
     while the other still needs it expanded. */
  .stage.slide-zone-expanded .panel {
    display: none;
  }

  .panel-section {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
  }

  /* 4-tier panel hierarchy: weekday, then the date row, then .clock, then
     .next-prayer. Weekday isolated on its own line frees the date row
     below from ever needing to share width with it — see .date-row. */
  .date-block {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 6px;
    margin-top: 10px;
    margin-bottom: 4px;
  }

  /* Accent-tinted card, matching .next-prayer's box treatment below (same
     color-mix approach, border-radius, so the two "cards" in this panel
     read as one visual language) — borderless and lighter (10% vs
     .next-prayer's 14%) so it reads as a quieter background wash rather
     than competing with .next-prayer's own bordered card. align-self:
     stretch overrides .date-block's own align-items: center (needed to
     keep .date-row centered) so this box spans the full panel width the
     same way .next-prayer does, rather than shrinking to hug the weekday
     text. */
  .date-block .weekday {
    align-self: stretch;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    font-family: var(--font-display);
    font-size: 2rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 1.5px;
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    border-radius: 5px;
    padding: 0px 10px;
  }

  /* Decorative flank either side of the weekday text — inline SVG (not
     <img>) so fill: currentColor picks up .weekday's own text color (no
     explicit color set here, so it resolves through to --text-primary,
     same as .clock) and tracks theme changes automatically. Height-capped
     only for now per design — Armen will hand-tune sizing in VS Code, so
     no width/aspect-ratio logic beyond letting the SVG's own viewBox keep
     it proportional. */
  .weekday-flank {
    height: 3px;
    width: auto;
    flex-shrink: 0;
  }

  /* Right-side instance is assets/flank.svg as-is; this mirrors the same
     markup for the left side rather than shipping a second, separately
     exported flipped asset. */
  .weekday-flank-left {
    transform: scaleX(-1);
  }

  /* Stacked (2-line) by default, single-line only once the panel is wide
     enough — same hybrid strategy as before weekday was isolated onto its
     own line above, just recalibrated: removing the weekday prefix from
     this row bought back some width, but not enough to go single-line
     unconditionally. Measured (real Amiri metrics, worst-case Hijri month
     spellings Jamadilakhir/Rabiulakhir/Jamadilawal, both dates at 1.7rem)
     against .panel's actual content width at 1920/1600/1366/1280/1024px
     kiosk stages: only 1920px (~468px of panel content width) clears the
     ~423px this row needs single-line; 1600px and below (~388px down to
     ~244px) fall back to the stacked rule here. No divider in the stacked
     state — vertical gap already separates the two dates, so a divider
     line there would be a stray unpaired underline. */
  .date-row {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
  }

  /* line-height 0.85 overrides Amiri's own taller-than-Georgia "normal"
     line-height (measured ~1.76x font-size vs an explicit multiplier here) —
     tested down to 0.85 with descender-heavy text with no clipping, since
     line-height:<number> is a direct multiplier of font-size, not of the
     font's own metrics. Shared between both dates — only color differs
     (Gregorian --accent, Hijri --text-muted; deliberately not equalized). */
  .date-block .gregorian,
  .date-block .hijri {
    font-family: var(--font-display);
    font-size: 1.8rem;
    font-weight: 600;
    line-height: 1;
  }

  .date-block .gregorian {
    color: var(--accent);
  }

  .date-block .hijri {
    color: var(--text-muted);
  }

  /* Tabular-fallback Hijri date (hijri_date not yet synced for this day) —
     deliberately reads with less confidence than a synced MUIS date.
     Color alone can no longer do that now that .hijri's own base color is
     already --text-muted (the secondary/bottom position) — italic keeps
     the distinction visible instead of silently becoming a no-op. */
  .date-block .hijri.is-estimate {
    font-style: italic;
  }

  /* Single-line mode — 430px is .panel's own content-box width (container
     queries measure content-box, excluding the 24px×2 padding included in
     clientWidth) needed to clear this row's ~423px worst case (measured:
     both dates at 1.7rem, worst-case Hijri month spellings), plus a few
     px of margin same as the pre-weekday-split version of this threshold
     had. Only 1920px-wide kiosk stages (~468px of panel content width)
     clear it among the widths this was tested against — 1600px and below
     stay on the stacked rule above. */
  @container panel (min-width: 430px) {
    .date-row {
      flex-direction: row;
      align-items: baseline;
      justify-content: center;
      gap: 0;
    }

    /* Faint vertical divider between the two dates, only meaningful once
       they're side by side — literal rgba(), not a theme token, same
       "hold up regardless of palette" reasoning as .fullscreen-toggle-btn
       above. margin-left/padding-left give the line breathing room on
       both sides instead of touching either date's text. */
    .date-row .hijri {
      margin-left: 12px;
      padding-left: 12px;
      border-left: 1px solid rgba(0, 0, 0, 0.15);
    }
  }

  /* Narrowest kiosk stages (~1024px viewport and below, ~269px or less of
     panel content width) — even stacked (one date per line, see .date-row
     above), the worst-case single Hijri line ("17 Jamadilakhir 1447" etc,
     measured at 1.7rem: ~255px) no longer fits within a ~244px-wide panel
     and wraps mid-phrase (e.g. the year orphaned onto its own third
     line). Dropping to 1.6rem here (measured: ~240px, 4px of margin) is
     the smallest step down that clears it — applies to both dates evenly
     rather than only the Hijri one, so they stay visually matched. */
  @container panel (max-width: 269px) {
    .date-block .gregorian,
    .date-block .hijri {
      font-size: 1.6rem;
    }
  }

  /* Live clock — 6.5vw (down from an initial 7vw ask, which measured as
     clipping "00:00:00" against .panel's real content width at every
     tested resolution: ~10px over at 1920, ~7px at 1600, ~5px at 1366,
     ~1px at 1024 — the panel does not render at a naive viewport/4;
     padding(24px/side) plus the 3:1 flex-grow split eat into it, same gap
     documented at the old 6vw tuning this replaced). 6.5vw measures a
     consistent ~1px of margin at all 4 of those widths (both the font
     size and the panel's own content width scale with viewport, so the
     margin holds proportionally rather than only at one reference
     resolution). font-variant-numeric: tabular-nums keeps digit widths
     equal so "00:00:00" is reliably the widest case to measure against. */
  .clock {
    font-family: var(--font-display);
    font-size: 6.5vw;
    letter-spacing: -2.5px;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    text-align: center;
    margin: 6px 0 12px;
  }

  hr.divider {
    border: none;
    border-top: 1px solid var(--divider);
    margin: 0.4rem 0;
  }

  /* Next prayer — accent-tinted card (background/border), matching the
     design reference's highlighted box treatment. Tints derived from
     --accent via color-mix rather than any literal color, so they still
     track whichever theme is active. Gap/padding/margin/text-align read
     directly from the reference; overrides .panel-section's default
     gap (0.4rem) with the reference's tighter 4px. */
  .next-prayer {
    display: flex;
    flex-direction: column;
    gap: 4px;
    text-align: center;
    background: color-mix(in srgb, var(--accent) 14%, transparent);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
    border-radius: 5px;
    padding: 10px 10px 12px;
    margin-bottom: 14px;
  }

  .next-prayer .label {
    font-size: 14px;
    font-weight: 700;
    color: var(--accent);
    text-transform: uppercase;
    letter-spacing: 1.5px;
  }

  /* Matches .next-prayer .countdown below (2rem) — second-largest tier
     after .clock, per the design reference's one large value for this
     block (the reference folds the prayer name into its small label
     instead of giving it a distinct large size; this keeps the existing
     3-element label/name/countdown structure but unifies their weight). */
  .next-prayer .name {
    font-size: 2.5rem;
    font-family: var(--font-display);
    line-height: 0.85;
  }

  /* No color override (was var(--accent)) — inherits the panel's base
     var(--text-primary), matching the reference: only the label above is
     accent-colored, the countdown number itself reads in plain ink. */
  .next-prayer .countdown {
    font-size: 2rem;
    font-variant-numeric: tabular-nums;
  }

  /* Rotating Quran / Hadith quote feed — the auto top margin that used to
     live here (pushing this element itself to the bottom of .panel) moved
     to .panel-region-c below once the A/B/C/D decomposition made this a
     grandchild of .panel rather than a direct child — see that rule's own
     comment for why. */
  .quote-feed {
    display: flex;
    flex-direction: column;
    padding-top: 14px;
    padding-right: 0.6rem;
  }

  /* Matches the design reference's small section label above its
     equivalent block — source text is "Reflection", CSS uppercases it. */
  .quote-feed-label {
    font-size: 10px;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 8px;
  }

  .quote-content {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    width: 100%;
    opacity: 1;
    transition: opacity 0.9s ease;
  }

  .quote-content.is-hidden {
    opacity: 0;
  }

  .quote-arabic {
    font-family: var(--font-arabic);
    font-size: 1.5rem;
    line-height: 1.9;
    color: var(--text-primary);
    direction: rtl;
    text-align: center;
  }

  .quote-text {
    font-size: 1.05rem;
    line-height: 1.5;
    color: var(--text-primary);
    text-align: center;
  }

  .quote-ref {
    font-size: 0.78rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    text-align: right;
  }

  /* "Powered by" mark — last element in every right panel (kiosk and the
     landing/about/features demo panels alike), below the quote feed. Baked-
     in brand PNG (deployable/assets/logo-horizontal.png), not theme-driven —
     same reasoning as .landing-brand-logo: this identifies Awqōt itself,
     not the mosque's chosen palette, so it stays fixed regardless of which
     theme is active on the rest of the panel. */
  .panel-powered-by {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--divider);
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* "Powered by [logo]" as one group on the left — space-between above
     splits .panel-powered-by into exactly two things (this group,
     .panel-powered-by-link), rather than treating "Powered by"/the logo/
     the link as three separately-spaced items. */
  .panel-powered-by-brand {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .panel-powered-by-logo {
    height: 20px;
    width: auto;
    display: block;
  }

  /* color/text-transform/letter-spacing inherit from .panel-powered-by so
     this reads as part of the same label, not a differently-styled link —
     underline only on hover/focus as the interactive affordance. */
  .panel-powered-by-link {
    color: inherit;
    text-decoration: none;
  }

  .panel-powered-by-link:hover,
  .panel-powered-by-link:focus-visible {
    text-decoration: underline;
  }

  /* Prayer timetable list */
  /* No longer margin-top:auto — .quote-feed owns the "push to bottom" job
     (see below), matching the reference's order (timetable, then the
     reflection/quote block); .panel-powered-by then follows immediately
     after it as the panel's true last element. */
  .timetable {
    display: flex;
    flex-direction: column;
  }

  /* Source attribution, required alongside the timetable it describes —
     same smallest-text/muted treatment as .panel-powered-by below (12px,
     --text-muted), the only other fixed-size (not rem-scaled) text in
     this panel, since both are the same kind of quiet, always-present
     footnote rather than primary display content. Static text, not
     JS-managed — appears identically in every copy of this panel (kiosk
     and the landing/about/features demo panels alike, same as
     .panel-powered-by), so it lives in each page's own HTML rather than
     being injected by prayer-panel.js. */
  .prayer-attribution {
    margin-top: 6px;
    font-size: 12px;
    color: var(--text-muted);
  }

  /* Padding on every row (not just .active) so the active row's background
     highlight below has room to read as a highlight rather than a strip
     hugging the text — and so rows don't shift height/alignment as the
     active prayer changes across the day. */
  .timetable .row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 1.5rem;
    padding: 8px 4px;
  }

  /* Active = current prayer window, per renderPrayerPanel's existing
     "most recently passed prayer" logic (already reused, not
     reimplemented here) — not the next upcoming one. Tint via color-mix
     off --accent, matching the design reference's highlighted-row
     treatment without any literal color. */
  .timetable .row.active {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
  }

  .timetable .row.active .prayer-name,
  .timetable .row.active .prayer-time {
    color: var(--accent);
    font-size: 1.5rem;
    font-weight: 700;
  }

  .prayer-name { color: var(--text-muted); font-weight: 500; }
  .prayer-time { font-variant-numeric: tabular-nums; font-weight: 500; }

  /* ===== Landing page (bare-domain branch, see tenant.js/index.html) —
     marketing page for awqot.app / www.awqot.app. Structure/order/spacing
     copied directly from the Claude Design reference file (Awqot
     Landing.dc.html): 75%/25% split, nav sticky *inside* the scroll column
     (not spanning the panel — the panel runs the full 100dvh with no nav
     above it, same as the reference), full-bleed slide-zone as the first
     thing visible, marketing sections below in the reference's exact
     order. Colors/fonts route through the app's existing theme tokens/
     typefaces rather than the reference's own hardcoded hex/Source Serif
     4/Manrope — same rule already applied to .panel. CTA buttons keep the
     already-established gold-background treatment (see landing-cta-button)
     rather than the reference's own primary-color buttons, since that gold
     treatment was itself an earlier deliberate customization on this app,
     not something the reference gets to silently override. */
  /* Marketing-site background (index.html's marketing view, about,
     features, privacy, terms — every page that uses this .landing shell)
     — deliberately NOT on body/html: this same stylesheet is also the
     live kiosk display's own (index.html's #kioskTemplate content, an
     entirely separate top-level container from .landing, populated by
     tenant-bootstrap.js at runtime, never both at once) — scoping to
     .landing specifically means this can't reach the kiosk template
     structurally, not just because something else happens to cover it.
     awqotbg.png is pale/subtle by design (faint astronomical line art on
     off-white, see assets/) — drawn as a background-image under existing
     content, not as extra opacity on any element, so it can't fade
     foreground text. Most sections have no background of their own
     (transparent, letting .landing's own background show through) —
     .about-why-free/.about-cta-card and similar solid-tinted sections
     still cover it there, which is fine: that's page rhythm, not a bug. */
  .landing {
    display: flex;
    width: 100dvw;
    height: 100dvh;
    overflow: hidden;
    background: var(--bg) url("../assets/awqotbg.png") center / cover no-repeat;
  }

  .landing-scroll-col {
    flex: 3;
    height: 100dvh;
    overflow-y: auto;
    overflow-x: hidden;
  }

  .landing-nav {
    position: sticky;
    top: 0;
    z-index: 20;
    height: 68px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 48px;
    background: color-mix(in srgb, var(--bg) 92%, transparent);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--divider);
  }

  .landing-brand {
    display: flex;
    align-items: center;
    text-decoration: none;
  }

  /* Baked-in brand PNG (deployable/assets/logo-horizontal.png) — fixed
     brand colors, not theme-driven (matches applyTheme(DEFAULT_THEME_ID)
     already forced on every page this appears on). Height tuned so the
     wordmark inside the image reads at roughly the nav's original text
     size, not the image's own full aspect-ratio height. */
  .landing-brand-logo {
    height: 40px;
    width: auto;
    display: block;
  }

  .landing-nav-links {
    display: flex;
    align-items: center;
    gap: 36px;
  }

  .landing-nav-links a {
    color: var(--text-primary);
    text-decoration: none;
    font-size: 15px;
    font-weight: 500;
  }

  .landing-nav-links a:hover {
    color: var(--accent);
  }

  /* Current-page indicator on the shared nav (About/Features pages) —
     bold + accent underline, matching the design reference's own
     active-link treatment exactly. Not shown on the landing page itself,
     since nothing in the nav represents "you are here" for it. */
  .landing-nav-links a.is-active {
    color: var(--text-primary);
    font-weight: 700;
    border-bottom: 2px solid var(--accent);
    padding-bottom: 2px;
  }

  .landing-nav-cta {
    background: var(--accent);
    color: var(--text-primary);
    padding: 10px 20px;
    border-radius: 3px;
    font-weight: 600;
    font-size: 14px;
  }

  .landing-nav-cta:hover {
    opacity: 0.9;
  }

  .landing-nav-toggle {
    display: none;
    font-size: 1.4rem;
    background: none;
    border: none;
    color: var(--text-primary);
    cursor: pointer;
  }

  /* Full-bleed, matching the real kiosk's .slide-zone dimensions/styling
     exactly (reuses .slides as-is — same background/canvas/placeholder
     rules) rather than the reference's own padded/rounded/shadowed card.
     Fills whatever viewport height the nav above it doesn't take, so it's
     the only thing visible (besides the nav) before any scrolling. */
  .landing-carousel-zone {
    position: relative;
    height: calc(100dvh - 68px);
  }

  .landing-carousel-zone .slides {
    position: absolute;
    inset: 0;
  }

  .landing-eyebrow {
    font-family: var(--font-display);
    font-size: 14px;
    line-height: 1.2;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--accent);
    font-weight: 700;
    margin-bottom: 16px;
  }

  .landing-hero {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 56px;
    align-items: stretch;
    padding: 96px 48px 80px;
  }

  .landing-hero-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .landing-hero-media {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
  }

  .landing-hero-media-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 150%;
    object-fit: cover;
  }

  .landing-hero h1 {
    font-family: var(--font-display);
    font-size: 52px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 24px;
  }

  .landing-lede {
    font-size: 19px;
    line-height: 1.65;
    color: var(--text-muted);
    margin: 0 0 36px;
  }

  .landing-hero-ctas {
    display: flex;
    gap: 16px;
  }

  .landing-cta-button {
    display: inline-block;
    background: var(--accent);
    color: var(--text-primary);
    text-decoration: none;
    font-weight: 600;
    font-size: 15px;
    padding: 15px 28px;
    border-radius: 3px;
  }

  .landing-cta-button:hover {
    opacity: 0.9;
  }

  .landing-cta-outline {
    display: inline-block;
    border: 1.5px solid var(--text-primary);
    color: var(--text-primary);
    text-decoration: none;
    font-weight: 600;
    font-size: 15px;
    padding: 14px 28px;
    border-radius: 3px;
  }

  .landing-cta-outline:hover {
    background: color-mix(in srgb, var(--text-primary) 8%, transparent);
  }

  .landing-problem {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 56px;
    align-items: stretch;
    padding: 0 48px 96px;
  }

  .landing-problem-media {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
  }

  .landing-problem-media-img {
    position: absolute;
    inset: 0;
    height: 100%;
    object-fit: cover;
  }

  .landing-problem-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .landing-problem p {
    font-size: 20px;
    line-height: 1.7;
    color: var(--text-primary);
    margin: 0;
  }

  .landing-problem p + p {
    margin-top: 16px;
  }

  .landing-features {
    padding: 0 48px 96px;
  }

  .landing-features h2 {
    font-family: var(--font-display);
    font-size: 37px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 48px;
  }

  .landing-feature-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 1px;
    background: var(--divider);
    border: 1px solid var(--divider);
    border-radius: 6px;
    overflow: hidden;
  }

  .landing-feature {
    background: var(--bg);
    padding: 32px;
  }

  .landing-feature-icon {
    display: block;
    width: 50px;
    height: auto;
    margin-bottom: 16px;
  }

  .landing-feature h3 {
    font-family: var(--font-display);
    font-size: 22px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 10px;
  }

  .landing-feature p {
    font-size: 15px;
    line-height: 1.6;
    color: var(--text-muted);
  }

  .landing-why-free {
    position: relative;
    padding: 96px 48px;
    background: var(--text-primary);
    color: var(--bg);
    overflow: hidden;
  }

  .landing-why-free::before {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(120deg, color-mix(in srgb, var(--text-primary) 90%, transparent) 0%, color-mix(in srgb, var(--text-primary) 55%, transparent) 100%),
      url("../assets/abstract.png") center / cover no-repeat;
  }

  .landing-why-free-inner {
    position: relative;
    z-index: 1;
  }

  .landing-why-free-inner .landing-eyebrow {
    color: var(--accent);
  }

  .landing-why-free-inner h2 {
    font-family: var(--font-display);
    font-size: 35px;
    line-height: 1.2;
    font-weight: 700;
    margin: 0 0 20px;
  }

  .landing-why-free-inner p {
    font-size: 18px;
    line-height: 1.7;
    color: color-mix(in srgb, var(--bg) 82%, transparent);
    margin: 0;
  }

  .landing-how {
    padding: 96px 48px;
  }

  .landing-how h2 {
    font-family: var(--font-display);
    font-size: 37px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 48px;
  }

  .landing-how-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 40px;
  }

  .landing-how-icon {
    display: block;
    width: 50px;
    height: auto;
    margin-bottom: 16px;
  }

  .landing-how-n {
    font-family: var(--font-display);
    font-size: 39px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--accent);
    margin-bottom: 14px;
  }

  .landing-how-title {
    font-size: 18px;
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 8px;
  }

  .landing-how-step p {
    font-size: 15px;
    line-height: 1.6;
    color: var(--text-muted);
  }

  .landing-any-mosque {
    padding: 0 48px 96px;
  }

  .landing-any-mosque-card {
    border: 1px solid var(--divider);
    border-radius: 6px;
    padding: 56px;
    display: grid;
    grid-template-columns: 1.3fr 0.9fr 1fr;
    align-items: center;
    column-gap: 40px;
  }

  .landing-any-mosque-text {
    max-width: 480px;
  }

  .landing-any-mosque-text h2 {
    font-family: var(--font-display);
    font-size: 30px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 16px;
  }

  .landing-any-mosque-text p {
    font-size: 16px;
    line-height: 1.65;
    color: var(--text-muted);
    margin: 0;
  }

  .landing-any-mosque-checklist {
    font-size: 22px;
    line-height: 2;
    color: var(--text-primary);
    font-weight: 600;
    white-space: nowrap;
  }

  /* Third column, same row as the text/checklist — not spanning/stacked
     below anymore. */
  .landing-any-mosque-media {
    border-radius: 8px;
    overflow: hidden;
  }

  .landing-any-mosque-media-img {
    display: block;
    width: 100%;
    height: auto;
  }

  .landing-signup {
    padding: 80px 48px 120px;
    text-align: center;
  }

  .landing-signup h2 {
    font-family: var(--font-display);
    font-size: 39px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 16px;
  }

  .landing-signup p {
    font-size: 17px;
    color: var(--text-muted);
    margin: 0 0 32px;
  }

  .landing-footer {
    padding: 48px;
    border-top: 1px solid var(--divider);
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  /* Baked-in brand PNG, same reasoning as .landing-brand-logo/
     .panel-powered-by-logo — fixed brand colors, not theme-driven. */
  .landing-footer-brand {
    height: 28px;
    width: auto;
    display: block;
  }

  .landing-footer-links {
    display: flex;
    gap: 28px;
  }

  .landing-footer-links a {
    color: var(--text-muted);
    text-decoration: none;
    font-size: 14px;
  }

  .landing-footer-copyright {
    font-size: 13px;
    color: var(--text-muted);
  }

  @media (max-width: 860px) {
    .landing-nav-toggle {
      display: block;
    }

    .landing-nav-links {
      display: none;
      position: absolute;
      top: 100%;
      left: 0;
      right: 0;
      flex-direction: column;
      align-items: flex-start;
      gap: 0;
      background: var(--bg);
      border-bottom: 1px solid var(--divider);
      padding: 0.5rem 2rem 1rem;
    }

    .landing-nav-links.is-open {
      display: flex;
    }

    .landing-nav-links a {
      padding: 0.6rem 0;
      width: 100%;
    }

    /* Shared footer, all three pages — display:flex;justify-content:
       space-between across three items (logo, links, copyright) never
       had a mobile override either; at 390px they don't fit in one row
       and visibly overlap ("Contact" colliding with "Made for Singapore
       mosques · 2026" — found scrolling to the bottom of the landing
       page during this audit). Same single-column stacking fix as
       everything else in this pass. */
    .landing-footer {
      flex-direction: column;
      align-items: flex-start;
      gap: 16px;
    }

    /* Mobile marketing view — ALL THREE pages built on .landing (landing,
       about, features). First-time implementation, not a regression:
       there was no prior mobile rule for .landing/.landing-scroll-col/
       #landingDemoPanel at all (confirmed via a full search of this
       file's @media blocks). The fixed-viewport/internal-scroll-container
       architecture used on desktop (and on the kiosk itself, including
       its own separate mobile view — see .stage .panel above, a
       different rule for a different element) doesn't fit a normal
       mobile webpage: without this, .landing's desktop flex-row (scroll-
       col beside the demo panel) just compresses both into the mobile
       viewport, so the scroll-col's real page content (hero heading,
       body copy, images) ends up in a ~130px-wide sliver visibly
       colliding with the panel next to it — the same "columns don't
       stack, text collides" bug as the grids below, just page-wide on
       About/Features instead of confined to one section. This becomes a
       single naturally-scrolling page instead, on all three: demo panel
       full width, rest of the content below it.

       html itself also needs the overflow override, not just body — the
       base rule is `html, body { height:100dvh; overflow:hidden; }`, one
       compound selector but two independent per-element rules. Overriding
       only body (as an earlier pass here did) leaves html's own
       height/overflow fully intact at every width; since html's overflow
       is explicitly non-visible, it governs the real document/viewport
       scroll directly and doesn't let body's overflow propagate up — so
       html alone was enough to block the whole page from scrolling even
       with body already fixed. Both need the override together. */
    html:has(.landing),
    body:has(.landing) {
      height: auto;
      overflow: visible;
    }

    .landing {
      flex-direction: column;
      height: auto;
      overflow: visible;
    }

    /* display:contents removes .landing-scroll-col's own box entirely —
       it carries zero visual styling even on desktop (flex/height/
       overflow only, see its base rule above), so nothing is lost here.
       This promotes nav/the hero/every marketing section/footer to be
       direct flex items of .landing itself, as true siblings of
       #landingDemoPanel instead of all being nested inside one shared
       box. Needed specifically so nav can get its own `order` independent
       of the panel below: nav and the rest of the page's content are
       both children of this one box, not siblings of the panel, so a
       flat `order` on the panel alone can only promote it above the
       *entire* box (nav included) or not at all — there was no way to
       slot it between nav and the rest of the content without first
       flattening the two into the same flex context. Scoped to this
       media query only — .landing-scroll-col's desktop flex/scroll
       behavior (line ~457) is a separate, unscoped rule this doesn't
       touch outside mobile. */
    .landing-scroll-col {
      display: contents;
    }

    #landingDemoPanel {
      flex: none;
      width: 100%;
      height: auto;
      overflow: visible;
      border-left: none;
      border-bottom: 1px solid var(--divider);
    }

    /* Root landing page only, layered on top of the general rules above:
       the kiosk carousel preview is hidden (not usable at phone width,
       and About/Features never had one to begin with). Nav goes first,
       then the demo panel, then the hero/rest of the page's content
       (which stays at the default order: 0 the flattened
       .landing-scroll-col children above all share, so it falls in after
       both of these without needing its own rule) — a landing-page-only
       stacking order, so About/Features keep their (hidden, per the rule
       below) panel in its natural DOM position instead. */
    .landing-carousel-zone {
      display: none;
    }

    .landing:has(.landing-carousel-zone) .landing-nav {
      order: -2;
    }

    .landing:has(.landing-carousel-zone) #landingDemoPanel {
      order: -1;
    }

    /* About/Features only (no .landing-carousel-zone present) — unlike
       root, which promotes its demo panel to the top on mobile, About/
       Features hide the panel entirely on mobile rather than stacking it
       above the page's own content. Same :has()-based per-page scoping
       convention as the rule directly above, just the inverse condition. */
    .landing:not(:has(.landing-carousel-zone)) #landingDemoPanel {
      display: none;
    }

    /* Every remaining multi-column grid on the landing page — these had
       no mobile override at all before this pass (same gap as the panel/
       carousel fix above, just not yet applied here), so each was still
       rendering its desktop N-column layout compressed into the mobile
       viewport: .landing-feature-grid/.landing-how-grid overflowed past
       the right edge, and .landing-any-mosque-card's 3 columns actually
       overlapped each other (illegible text — the bug reported). Plain
       single-column stacking for the text-only grids; the two hero-style
       grids (.landing-hero/.landing-problem) also need their media
       wrapper given an explicit aspect-ratio, since the fill image inside
       is position:absolute and has no intrinsic height of its own to
       stack with once it's no longer sharing a grid row with the text
       column (that's what previously gave it a height, via align-items:
       stretch matching the row to the text's height — collapsing to one
       column removes that row-sharing entirely). */
    .landing-hero {
      grid-template-columns: 1fr;
    }

    .landing-hero-media {
      aspect-ratio: 4 / 3;
    }

    .landing-problem {
      grid-template-columns: 1fr;
    }

    .landing-problem-media {
      aspect-ratio: 4 / 3;
    }

    .landing-feature-grid {
      grid-template-columns: 1fr;
    }

    .landing-how-grid {
      grid-template-columns: 1fr;
    }

    .landing-any-mosque-card {
      grid-template-columns: 1fr;
    }
  }

  /* ===== About / Features pages (deployable/about/, deployable/features/)
     — static routes, not templates cloned via tenant-bootstrap.js (see
     project notes: cPanel has no server middleware, so these are their own
     physical index.html files under clean-URL directories, reusing the
     landing page's .landing/.landing-nav/.panel structure and this same
     stylesheet). Structure/copy/spacing copied directly from the Claude
     Design reference files (Awqot About.dc.html / Awqot Features.dc.html);
     colors/fonts route through the same theme tokens as the rest of the
     app rather than the reference's own hardcoded hex/Source Serif
     4/Manrope — same rule already applied to .landing. Section classes
     below are shared between both pages where the reference's own markup
     is identical (hero, closing CTA); page-specific classes are prefixed
     about-/features-. */
  .static-hero {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 56px;
    align-items: stretch;
    padding: 96px 48px 88px;
  }

  .static-hero-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .static-hero-text p {
    font-size: 19px;
    line-height: 1.7;
    color: var(--text-primary);
    margin: 0;
  }

  .static-hero-text p + p {
    margin-top: 24px;
  }

  .static-hero-media {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
  }

  .static-hero-media-img {
    position: absolute;
    inset: 0;
    width: 170%;
    height: 100%;
    object-fit: cover;
  }

  .static-hero h1 {
    font-family: var(--font-display);
    font-size: 48px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 24px;
  }

  .static-cta {
    padding: 16px 48px 96px;
    text-align: center;
  }

  .static-cta.about-cta {
    padding-bottom: 120px;
  }

  .static-cta h2 {
    font-family: var(--font-display);
    font-size: 35px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 16px;
  }

  .static-cta p {
    font-size: 16px;
    color: var(--text-muted);
    margin: 0 0 32px;
  }

  /* Privacy Policy / Terms of Usage — plain long-form text, not a
     marketing section, so this reuses the shared nav/footer chrome but
     none of the image+copy section styling built for .static-hero,
     .about-*, .features-*, etc. Centered single column, generously
     capped width for readability at long paragraph lengths. */
  .legal-page {
    max-width: 760px;
    margin: 0 auto;
    padding: 64px 24px 96px;
  }

  .legal-page h1 {
    font-family: var(--font-display);
    font-size: 2.5rem;
    line-height: 1.15;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 8px;
  }

  .legal-page .legal-updated {
    color: var(--text-muted);
    font-size: 0.9rem;
    margin: 0 0 40px;
  }

  .legal-page h2 {
    font-family: var(--font-display);
    font-size: 1.4rem;
    font-weight: 700;
    color: var(--text-primary);
    margin: 40px 0 12px;
  }

  .legal-page p,
  .legal-page li {
    font-size: 16px;
    line-height: 1.6;
    color: var(--text-primary);
  }

  .legal-page p {
    margin: 0 0 16px;
  }

  .legal-page ul,
  .legal-page ol {
    margin: 0 0 16px;
    padding-left: 24px;
  }

  /* Flags a [CONFIRM: ...] placeholder from the source content (or, for
     now, the page's own missing-content state) as visibly unfinished —
     deliberately loud rather than blending in, so nobody mistakes a
     pending page for a published one. */
  .legal-todo {
    display: block;
    background: color-mix(in srgb, var(--accent) 15%, transparent);
    border: 1px solid var(--accent);
    border-radius: 6px;
    padding: 12px 16px;
    margin: 16px 0;
    font-size: 15px;
    font-weight: 600;
    color: var(--text-primary);
  }

  .legal-todo::before {
    content: "TODO — Armen to confirm: ";
    color: var(--accent);
  }

  /* About page */
  .about-why-singapore {
    margin-top: 88px;
  }

  .about-why-singapore h2 {
    font-family: var(--font-display);
    font-size: 30px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 16px;
  }

  .about-why-singapore p {
    font-size: 17px;
    line-height: 1.7;
    color: var(--text-muted);
    margin: 0;
  }

  .about-why-free {
    position: relative;
    padding: 88px 48px;
    background: var(--text-primary);
    color: var(--bg);
    overflow: hidden;
  }

  .about-why-free::before {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(120deg, color-mix(in srgb, var(--text-primary) 90%, transparent) 0%, color-mix(in srgb, var(--text-primary) 55%, transparent) 100%),
      url("../assets/abstract.png") center / cover no-repeat;
  }

  .about-why-free-inner {
    position: relative;
    z-index: 1;
    max-width: 640px;
  }

  .about-why-free-inner .landing-eyebrow {
    color: var(--accent);
  }

  .about-why-free-inner h2 {
    font-family: var(--font-display);
    font-size: 32px;
    line-height: 1.2;
    font-weight: 700;
    margin: 0 0 20px;
  }

  .about-why-free-inner p {
    font-size: 17px;
    line-height: 1.7;
    color: color-mix(in srgb, var(--bg) 82%, transparent);
    margin: 0;
  }

  /* Merged "What We Believe" + "The Muwaqqit" into this one section —
     .about-believe (the old what-we-believe.png-alone diptych, near-even
     1fr/1fr split) is gone entirely; this replaces it, keeping the name
     since the body copy is the Muwaqqit narrative carried over unchanged,
     just wearing the "What We Believe" eyebrow/heading now (see
     about/index.html). Column ratio flips to 1fr/2fr — image column now
     genuinely secondary to the (much longer) copy, not co-equal with it.
     align-items:center, not stretch: .about-muwaqqit-media below sizes
     itself via its own aspect-ratio, same reasoning as before — stretch
     would fight that regardless of column width. */
  .about-muwaqqit {
    display: grid;
    grid-template-columns: 1fr 2fr;
    column-gap: 56px;
    align-items: center;
    padding: 88px 48px;
  }

  /* Single image now — this section briefly carried a composite
     muwaqqit.png + what-we-believe.png overlay treatment (see git
     history), removed again: the overlay's own file is gone from
     assets/ entirely, so no .about-muwaqqit-media-overlay rule exists to
     maintain either. position:relative stays even with nothing absolutely
     positioned inside it beyond .about-muwaqqit-media-img itself — that
     img still needs a positioned ancestor for inset:0 to resolve against. */
  .about-muwaqqit-media {
    position: relative;
    aspect-ratio: 3 / 4;
  }

  /* aspect-ratio:3/4 on the wrapper above matches muwaqqit.png's real
     1086x1448 file exactly (confirmed) — object-fit:cover here never
     actually needs to crop anything as a result, archway top included;
     this is the same already-working no-crop technique the standalone
     Muwaqqit section used (confirmed via real rendered measurements: box
     and image came out pixel-identical, zero cropping), just carried
     into a narrower 1fr (of 1fr/2fr) column instead of 1fr of 1fr/1fr —
     the technique doesn't care about column width, only that the WRAPPER's
     own aspect-ratio matches the source file's, which it still does here. */
  .about-muwaqqit-media-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 8px;
  }

  .about-muwaqqit-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .about-muwaqqit-text h2 {
    font-family: var(--font-display);
    font-size: 30px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 24px;
  }

  /* Same 17px/1.7 body-text size this page already uses for prose outside
     the hero (.about-why-singapore p) — the hero's own .static-hero-text p
     is 19px, a deliberately larger lead-paragraph treatment for the
     page's opening, not the convention a later section should match.
     Full-strength --text-primary (not .about-why-singapore's muted tone)
     since this is substantive narrative content meant to be read in full. */
  .about-muwaqqit-text p {
    font-size: 17px;
    line-height: 1.7;
    color: var(--text-primary);
    margin: 0;
  }

  .about-muwaqqit-text p + p {
    margin-top: 20px;
  }

  /* Relocated from the page's old standalone .static-cta.about-cta section
     (removed from about/index.html — see git history) into this column,
     directly under the third paragraph. Pure spacing wrapper now — all
     the actual visual treatment (background, padding, radius, text color)
     lives on .about-cta-card nested inside it (below); keeping this one
     scoped to margin-top ONLY avoids a same-specificity rule-ordering
     fight between ".about-muwaqqit-cta h2" and ".about-cta-card h2" that
     a first pass here had (both one class + one element = identical
     specificity, meaning whichever happened to sit later in the file
     would silently win — fragile, not obvious from reading either rule
     alone). One rule owns each concern instead. */
  .about-muwaqqit-cta {
    /* More than the 20px p+p rhythm above (this is a distinct block, not
       another paragraph in the same flow) but well under
       .about-why-singapore's 88px full-subsection gap (this is still the
       same section, not a new one). */
    margin-top: 40px;
  }

  /* Deliberately NOT reusing .static-cta/.static-cta h2/.static-cta p
     here: that class is shared with Features' own still-standalone,
     still-centered CTA (.static-cta.features-cta) — .static-cta's own
     text-align:center is exactly the alignment this card needs to NOT
     have now that it's nested in a left-aligned text column, and editing
     the shared rule to fix that here would un-center Features' CTA too.
     Writing fresh, locally-scoped rules instead means Features is
     untouched by construction, not by carefully avoiding a shared
     selector. Left-align falls out for free this way — nothing here sets
     text-align at all, so it just takes the card's own default block
     alignment, no override needed. .landing-cta-button IS still reused
     directly for the button itself: that class carries no section-level
     assumptions (no text-align, no centering) of its own, only the
     button's own look — a genuinely shared, safe-to-reuse component
     class, unlike .static-cta.

     #0B4A44 hardcoded (not var(--color-primary), even though they're the
     same value right now) per this page's own existing convention of
     literal hex for text/surfaces meant to stay Awqōt's fixed brand color
     regardless of any active mosque theme (see the plain "#fff" instances
     already elsewhere in this file) — moot on THIS page specifically
     (about/index.html never loads theme.js, confirmed, so --color-primary
     can't actually change here), but hardcoding is what was asked for and
     costs nothing to honor.

     No explicit width here: .about-muwaqqit-text is a column-direction
     flex container with the default align-items:stretch (nothing
     overrides it), so this card — like the paragraphs above it — already
     fills exactly the 2fr text column's own width for free. It can't
     bleed into the 1fr image column (that's a separate grid track
     entirely) or expand past the text column without something
     explicitly widening it, which nothing here does. */
  .about-cta-card {
    background: #0B4A44;
    padding: 40px 48px;
    border-radius: 8px;
  }

  .about-cta-card h2 {
    font-family: var(--font-display);
    font-size: 30px;
    line-height: 1.2;
    font-weight: 700;
    color: #fff;
    margin: 0 0 12px;
  }

  /* Same 82%-opacity-white-on-dark treatment .about-why-free-inner p
     already uses for muted text on this page's other dark section — that
     one reaches it via color-mix(in srgb, var(--bg) 82%, transparent)
     (theme-aware, since --bg is a var); this one is the literal-hex
     equivalent (rgba, not color-mix+var) for the same "hardcoded, not
     theme-driven" reason the card's own background is. Smaller than the
     surrounding narrative paragraphs (17px above) — same role .static-cta
     p always played (a supporting caption under the heading, not more
     body prose); this is what visually marks the CTA as its own distinct
     block rather than a fourth paragraph continuing the story. */
  .about-cta-card p {
    font-size: 16px;
    line-height: 1.5;
    color: rgba(255, 255, 255, 0.82);
    margin: 0 0 20px;
  }

  /* Features page */
  .features-list {
    padding: 0 48px 32px;
  }

  /* Hero heading + Prayer Times + Hijri Rollover (the item with the "Why
     This Matters" callout) sit in a left text column; the right column is
     a reserved media slot for a future large image spanning that whole
     column's height — header included, not just the two feature items
     below it — via grid's default align-items:stretch (no fixed height
     needed). No divider between the heading and the first item — the
     image takes over that job visually instead. */
  .features-hero-highlight {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 48px;
    padding: 0 48px;
  }

  .features-hero-highlight-text {
    display: flex;
    flex-direction: column;
  }

  .features-hero {
    padding-top: 96px;
  }

  .features-hero h1 {
    font-family: var(--font-display);
    font-size: 48px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0;
  }

  .features-highlight-items {
    margin-top: 64px;
  }

  /* No top divider on the first item — the heading above it is now part
     of the same visual block as the media image beside it. */
  .features-highlight-items .features-item:first-child {
    border-top: none;
  }

  .features-highlight-media {
    padding: 96px 0 28px;
    display: flex;
  }

  /* Same class works whether this is still the empty placeholder <div> or
     has been swapped for <img class="features-highlight-media-box"
     src="..." alt="..."> — width/height/object-fit only affect replaced
     elements (img), so they're inert but harmless on the placeholder div. */
  .features-highlight-media-box {
    flex: 1;
    min-height: 200px;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 8px;
    display: block;
    background: color-mix(in srgb, var(--text-primary) 6%, transparent);
  }

  /* 2-column grid for the 6 simpler feature entries below .features-highlight.
     Fills the scroll column's own width (no max-width cap, unlike the
     old single-column layout) — the scroll column is already a separate
     flex child from .panel, so this never reaches the right panel
     regardless. border-bottom lives here, not on :last-child below, so it
     closes off both columns' last row evenly instead of just one. */
  .features-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    column-gap: 48px;
    border-bottom: 1px solid var(--divider);
  }

  .features-item {
    padding: 28px 0;
    border-top: 1px solid var(--divider);
  }

  .features-item h3 {
    font-family: var(--font-display);
    font-size: 23px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 8px;
  }

  .features-item p {
    font-size: 16px;
    line-height: 1.65;
    color: var(--text-muted);
    margin: 0;
  }

  .features-item p + .features-callout {
    margin-top: 16px;
  }

  .features-callout {
    background: color-mix(in srgb, var(--text-primary) 5%, transparent);
    border-left: 3px solid var(--accent);
    border-radius: 0 4px 4px 0;
    padding: 16px 20px;
  }

  .features-callout-label {
    font-size: 12px;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    color: var(--accent);
    font-weight: 700;
    margin: 0 0 8px;
  }

  .features-callout p {
    font-size: 15px;
    line-height: 1.7;
    color: var(--text-primary);
    margin: 0;
  }

  /* Comparison/differentiator section (vs. sharing announcement slides via
     a plain Canva link) — light --panel-bg tint (same token the kiosk's
     own .slides/.panel use, the closest existing precedent for
     section-level subtle shading on this codebase; every other
     color-mix() background here is component-level — a button hover, a
     media placeholder, a callout card — not a full section), not
     .features-coming-soon's dark --text-primary band directly below it:
     two dark sections back to back would merge into one visual block,
     so this is the middle step of three visually distinct bands (plain
     white feature list → light-tinted comparison → dark Coming Soon).
     Text colors are unchanged from a plain background (var(--text-primary)/
     var(--text-muted) both still read correctly against this light tint,
     no light/dark swap needed the way a dark background would require).
     Media-left/text-right split reuses the same grid-template-columns/
     column-gap/align-items:stretch + absolute-fill/object-fit:cover
     image treatment this page's own diptych sections were already built
     on (originally shared with the About page's now-merged/reworked
     .about-believe section) rather than inventing a new pattern —
     landscape.png (despite the filename, an
     actual 1:1 square image, not a wide photo) fills whatever height the
     text column ends up needing via align-items:stretch regardless of its
     own aspect ratio. Bullets reuse .features-item/h3/p verbatim (no new
     rules needed there) — same heading+paragraph convention as every
     other feature entry on this page. */
  .features-compare {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 56px;
    align-items: stretch;
    padding: 64px 48px 96px;
    background: var(--panel-bg);
  }

  .features-compare-media {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
  }

  .features-compare-media-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .features-compare-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .features-compare-text h2 {
    font-family: var(--font-display);
    font-size: 30px;
    line-height: 1.2;
    font-weight: 700;
    color: var(--text-primary);
    margin: 0 0 12px;
  }

  .features-compare-text > p {
    font-size: 16px;
    color: var(--text-muted);
    margin: 0 0 36px;
  }

  .features-compare-list {
    border-bottom: 1px solid var(--divider);
  }

  .features-coming-soon {
    padding: 64px 48px 88px;
    background: var(--text-primary);
    color: var(--bg);
  }

  /* No max-width — fills the scroll column's full width like
     .features-list/.features-grid above it, so the 3-column grid below
     doesn't leave a large empty gap on the right on wide viewports. */
  .features-coming-soon-inner {
    width: 100%;
  }

  .features-coming-soon-inner .landing-eyebrow {
    color: var(--accent);
  }

  .features-coming-soon-inner h2 {
    font-family: var(--font-display);
    font-size: 30px;
    line-height: 1.2;
    font-weight: 700;
    margin: 0 0 12px;
  }

  .features-coming-soon-inner > p {
    font-size: 16px;
    color: color-mix(in srgb, var(--bg) 75%, transparent);
    margin: 0 0 36px;
  }

  .features-coming-soon-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 32px;
  }

  .features-coming-soon-icon {
    display: block;
    width: 50px;
    height: auto;
    margin-bottom: 16px;
  }

  .features-coming-soon-item h3 {
    font-size: 17px;
    font-weight: 700;
    color: var(--accent);
    margin: 0 0 10px;
  }

  .features-coming-soon-item p {
    font-size: 15px;
    line-height: 1.6;
    color: color-mix(in srgb, var(--bg) 82%, transparent);
    margin: 0;
  }

  .features-request {
    padding: 40px 48px;
    text-align: center;
  }

  .features-request p {
    font-size: 16px;
    color: var(--text-muted);
    margin: 0;
  }

  .features-request a {
    color: var(--text-primary);
    font-weight: 600;
  }

  /* Same gap as the landing page's grids above — none of these had a
     mobile override at all before this pass, so each was still rendering
     its desktop N-column layout compressed into the mobile viewport.
     .static-hero is About-page-only (Features has its own separate
     .features-hero-highlight); .about-muwaqqit is About-only;
     .features-hero-highlight/.features-grid/.features-coming-soon-grid
     are Features-only — none of these class names are shared with the
     landing page's own grids, so no :has()-style scoping is needed here.
     .static-hero-media needs the same explicit aspect-ratio as landing-
     hero-media/landing-problem-media above, for the same reason
     (position:absolute fill image, no intrinsic height once it's not
     sharing a grid row with the text column anymore).
     .features-highlight-media-box already has its own min-height:200px
     fallback (see its rule above), so it degrades safely without needing
     an explicit override here. */
  @media (max-width: 860px) {
    .static-hero {
      grid-template-columns: 1fr;
    }

    .static-hero-media {
      aspect-ratio: 4 / 3;
    }

    /* .about-muwaqqit-media already carries its own aspect-ratio:3/4 at
       the desktop rule above (it's not stretch-derived the way
       .features-compare-media is), so the only thing needed here is the
       single-column stack — same "aspect-ratio already matches the real
       file, nothing to override" reasoning .features-compare's 1:1
       mobile override uses for landscape.png. */
    .about-muwaqqit {
      grid-template-columns: 1fr;
    }

    .features-hero-highlight {
      grid-template-columns: 1fr;
    }

    /* aspect-ratio:1/1, not the 4/3 every other media column above uses —
       landscape.png actually is a 1:1 square image, so this crops nothing
       instead of fighting its real shape. */
    .features-compare {
      grid-template-columns: 1fr;
    }

    .features-compare-media {
      aspect-ratio: 1 / 1;
    }

    .features-grid {
      grid-template-columns: 1fr;
    }

    .features-coming-soon-grid {
      grid-template-columns: 1fr;
    }
  }

  /* ===== Not-found branch (subdomain slug that doesn't resolve to any
     active mosque) — deliberately distinct from both the kiosk and the
     landing page, not a fallback to either (see tenant.js). */
  .not-found {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100dvw;
    height: 100dvh;
    text-align: center;
    padding: 2rem;
    gap: 1rem;
  }

  .not-found h1 {
    font-family: var(--font-display);
    font-size: 1.95rem;
    line-height: 1.2;
    color: var(--text-primary);
  }

  .not-found p {
    color: var(--text-muted);
    max-width: 40ch;
  }
