/* The generic app shell: frame, top bar, sheets and the panel outlet, page
   chrome, the phone shell and its pilots. index.html loads it right after
   tokens.css (its own <link>, not an @import here: an @import is fetched only
   once this file has arrived — review N-12), then app.css (Kassza's own
   screens), then ds.css — at equal specificity a rule here loses to both. */

/* Self-hosted Plus Jakarta Sans (latin + latin-ext), replacing the Google
   Fonts CDN <link> that used to live in index.html + the @import that used
   to live in tokens.css. Google serves the SAME file for every static weight
   we requested (400/500/600/700/800) per subset — it's a variable font — so
   one download per subset covers the whole range via `font-weight: 200 800`.
   latin-ext is load-bearing, not optional: Hungarian ő/ű (U+0151, U+0171)
   live there, not in latin — a latin-only subset falls back silently on
   exactly those two letters. unicode-range values copied verbatim from
   Google's css2 response so the browser still only downloads the subset it
   needs. */
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 200 800;
    font-display: swap;
    src: url(../fonts/plus-jakarta-sans-latin-ext.woff2) format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Plus Jakarta Sans';
    font-style: normal;
    font-weight: 200 800;
    font-display: swap;
    src: url(../fonts/plus-jakarta-sans-latin.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

html, body {
    margin: 0;
    padding: 0;
    font-family: var(--et-font-sans);
    background-color: var(--et-color-bg);
    color: var(--et-color-ink);
    /* Android WebView still paints its blue tap-highlight on tappable
       elements (Chrome/PWA no longer do) — the DS has its own pressed
       states, so the native shell showed doubled feedback without this. */
    -webkit-tap-highlight-color: transparent;
}

/* The Android WebView applies :focus-visible on plain taps (Chrome's touch
   heuristic suppresses it), so every tapped control flashed the DS's 2px
   accent focus ring in the native shell. Suppressed ONLY there — web and PWA
   keep full keyboard-focus affordances. The .et-native-shell class is stamped
   by app-init.js when Capacitor reports a native platform. */
html.et-native-shell :focus-visible {
    outline: none;
}

/* ──────────── App frame (Home redesign 2026-06-29) ────────────
   Wraps the app shell in a 1240px max-width "card" — rounded corners,
   drop shadow, floating on a light-neutral body backdrop. Matches the
   mockup's "app-as-card" presentation. Only kicks in at ≥ md so mobile
   keeps the edge-to-edge layout it needs. */
@media (min-width: 960px) {
    html, body {
        background-color: var(--et-frame-bg);
    }
    body > #app > .app-shell,
    body > #app .app-shell {
        max-width: 1240px;
        margin: 24px auto;
        background: var(--et-color-bg, #FAF8F3);
        border-radius: 20px;
        box-shadow: 0 20px 48px rgba(0, 0, 0, 0.13);
        overflow: hidden;
        min-height: calc(100vh - 48px);
    }
    /* Dev-only current-user chip: absolute-centered on the header so it never
       gets crowded by the trailing controls. Renders only when DevAuth is
       enabled (production headers stay untouched); below this width there is
       no room for it next to the group pill, so it is hidden instead — see
       the phone block under .app-topbar. */
    .et-pill.et-appbar-dev {
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        margin: 0;
        z-index: 1;
        pointer-events: none;
    }
}

/* ──────────── App chrome: the DS TopBar (MainLayout) ────────────
   MainLayout renders the DS .et-topbar DOM with the real interactive pieces
   inside (ds.css owns the atom's looks). Only the app-shell specifics live
   here — the DS atom's own phone branch sits at 640px, while the app shell's
   boundary is 960px, so the app-side branch gets its own class rather than
   editing the DS rule the gallery previews.

   Sticky, not fixed: inside the ≥960px card frame (overflow:hidden) sticky
   resolves to "stays put", which is what the mockup's in-card header wants;
   on a phone there is no such ancestor, so the header follows the scroll and
   .main-content needs no top padding to clear it either way. position:sticky
   is also a positioned value, so it doubles as the containing block the
   absolutely-centered dev chip needs.

   z-index 1102 = one above .et-fab (1101), and it matters more than the bar's
   own painting: a sticky element is a stacking context, so the bell's and the
   profile menu's dropdowns — including their phone bottom sheets, which are
   fixed to the BOTTOM of the screen — can never paint above their host bar's
   level however high their own z-index is. At the AppBar's old 1100 the FAB
   floated on top of an open sheet. Still below the panel outlet (1350),
   .et-panel (1400) and .et-modal-overlay (1500). */
.app-topbar {
    position: sticky;
    top: 0;
    z-index: 1102;
}
.app-topbar-brand { cursor: pointer; }
.app-topbar-brand:focus-visible {
    outline: 2px solid var(--et-color-accent);
    outline-offset: 2px;
    border-radius: 10px;
}

/* Profile menu trigger: 38x38 plum disc showing the user's initial. Rendered
   as the EtDropdown <Trigger> — a div rather than a button so no global
   button rule can reshape the circle. The 6px sits on top of .app-topbar's
   own flex gap (ds.css) and widens the bell↔avatar gap a little past the
   switcher↔bell gap on desktop; zeroed below 960px so the switcher, the
   bell and the avatar all read as the same uniform gap on a phone. */
.et-appbar-avatar { margin-left: 6px; }
.et-appbar-avatar-circle {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    /* Follows the palette via the shared CTA token. This used to be its own
       plum literal — a lighter three-stop ramp than the CTA's — which meant the
       header disc stayed plum while the buttons beside it changed color. The
       disc is not a person's EtAvatar (those tones are per-user identity and
       deliberately palette-independent); it is header chrome, so it tracks the
       chrome. Owner decision 2026-09-07: reuse the CTA gradient for now rather
       than mint a lighter per-palette variant. */
    background: var(--et-cta-gradient);
    color: var(--et-cta-ink);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 1rem;         /* 16px */
    letter-spacing: -0.01em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: var(--et-avatar-ring);
    transition: filter 0.12s ease, transform 0.12s ease;
    position: relative;
    overflow: hidden;
}
/* Uploaded profile picture overlay — mirrors .et-avatar-img (ds.css); the
   initial underneath stays as the fallback while it loads / on error. */
.et-appbar-avatar-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}
.et-appbar-avatar-circle:hover { filter: brightness(1.06); }
.et-appbar-avatar-circle:active { transform: scale(0.96); }

/* Store-link pill (StoreLinkPill.razor) — "App letöltése". The JS
   platform check (etStore.platform, app-init.js) keeps it out of the
   installed PWA, the native shell and desktop browsers; the >=960px block
   below hides it on wide viewports too (an Android tablet in landscape gets
   the full desktop chrome, and the ScreenDumps harness answers 'android'
   for every dump — the gate is what keeps the desktop dumps honest). Same
   hover treatment as .et-group-switcher for a consistent pill vocabulary in
   the header. */
.et-appbar-store {
    display: inline-flex;
    align-items: center;
    border: 1.5px solid transparent;
    border-radius: 999px;
    background: var(--et-color-surface-2);
    padding: 8px 14px;
    /* The pill is an <a> (store listing) or a <button> (coming-soon toast) —
       `font: inherit` before the font-* overrides normalizes the button to
       the anchor's metrics, same trick as .et-group-switcher. */
    font: inherit;
    font-weight: 700;
    font-size: 0.8125rem;   /* 13px */
    color: var(--et-color-ink);
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    transition: border-color 0.15s ease;
}
.et-appbar-store:hover {
    border-color: var(--et-color-border-strong);
}

/* Phone chrome (< md). What used to be four MudHidden gates is these few
   declarations: brand tile only, group pill kept (the canvas requires it —
   it is the only group affordance left since BottomNav went), no header CTA,
   no dev chip. The bell and the avatar stay at every width. */
@media (max-width: 959.98px) {
    .app-topbar .et-topbar-name,
    .app-topbar .app-topbar-cta,
    .app-topbar .et-appbar-dev { display: none; }
    /* The group-switcher label is hidden on phones altogether (its old 40vw
       ellipsis clamp went with it) — see the switcher's own phone block
       in app.css ("Compact trigger"). */
    /* Bell↔avatar gap loses its extra 6px below 960px so switcher, bell and
       avatar sit at the topbar's uniform flex gap ("azonos margók") — see
       .et-appbar-avatar above. */
    .app-topbar .et-appbar-avatar { margin-left: 0; }
}

/* Desktop order-restore (>=960px). MainLayout emits the bell and the avatar
   directly after GroupSwitcher now (so a phone header reads switcher, bell,
   avatar at one uniform gap — see the max-width block above), which would
   otherwise put them ahead of the "+ Új költség" CTA in source order on a
   wide screen too. `order` repaints the desktop row back to its old visual
   sequence — brand | switcher pill … CTA, store pill, bell, avatar — while
   .et-topbar-right's own margin-left:auto still pushes it (and everything
   ordered after it) to the trailing edge. */
@media (min-width: 960px) {
    .app-topbar .et-topbar-right   { order: 1; }
    .app-topbar .et-notif-dropdown { order: 2; }
    .app-topbar .et-appbar-avatar  { order: 3; }
    /* Phone-only affordance — see the .et-appbar-store comment above. */
    .app-topbar .et-appbar-store   { display: none; }
}

/* Profile menu section label ("Váltás másik felhasználóra"). Outside the
   desktop frame query on purpose — the EtDropdown morphs into a bottom
   sheet on a phone and the label goes with it. The DS GroupMenu vocabulary
   has rows and dividers but no subheader. */
.et-profile-menu-section {
    padding: 8px 12px 4px;
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--et-color-ink-3);
}


/* Last-resort error notice (shown by Blazor when an exception escapes the
   in-app ErrorBoundary). Rendered as a rounded, inset floating card rather than
   a harsh full-width system bar so it reads as friendly and is mobile-tidy. */
#blazor-error-ui {
    background: #b32424;
    color: var(--et-cta-ink);
    bottom: 16px;
    left: 50%;
    transform: translateX(-50%);
    box-shadow: 0 4px 16px rgba(0,0,0,0.25);
    display: none;
    padding: 0.75rem 2.25rem 0.75rem 1rem;
    position: fixed;
    width: calc(100% - 32px);
    max-width: 460px;
    border-radius: 12px;
    font-size: 0.9rem;
    line-height: 1.35;
    z-index: 1300;
}

#blazor-error-ui .blazor-error-text { display: block; margin-bottom: 0.4rem; }
#blazor-error-ui .reload {
    color: var(--et-cta-ink);
    font-weight: 600;
    text-decoration: underline;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.6rem;
    top: 0.5rem;
}

/* Boot splash — one block styles BOTH the static #kk-boot-splash in
   index.html and the Blazor-side BootSplash component (identical markup), so
   the static→Blazor handoff is pixel-identical. z-index sits above the app
   chrome (topbar 1102) but below #blazor-error-ui (1300): a boot failure
   must surface over the splash. Colors come from theme tokens, which resolve
   correctly by first paint either way: pre-paint-theme.js has stamped
   [data-theme]/[data-palette] for a user with stored preferences, and an
   unstamped root already resolves to the defaults (light plum). */
.kk-boot-splash {
    position: fixed;
    inset: 0;
    z-index: 1250;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
    background: var(--et-color-bg, #FAFAF7);
}

/* The class's display:flex above beats the UA stylesheet's [hidden] rule —
   restate it explicitly or the hidden attribute silently stops working. */
.kk-boot-splash[hidden] { display: none; }

.kk-boot-splash-logo {
    width: 64px;
    height: 64px;
    border-radius: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 800;
    font-size: 24px;
    letter-spacing: 0.02em;
    color: var(--et-cta-ink);
    background: var(--et-color-accent, #7A4E7A);
    animation: kk-splash-pulse 1.6s ease-in-out infinite;
}

.kk-boot-splash-dots { display: flex; gap: 8px; }

.kk-boot-splash-dots span {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--et-color-accent, #7A4E7A);
    opacity: 0.35;
    animation: kk-splash-dot 1.2s ease-in-out infinite;
}

.kk-boot-splash-dots span:nth-child(2) { animation-delay: 0.15s; }
.kk-boot-splash-dots span:nth-child(3) { animation-delay: 0.3s; }

/* Escape hatch for a wedged boot/auth leg: invisible for the first ~12s,
   then fades in. Pure CSS (animation-delay) — no JS timer to leak across
   the static/Blazor splash handoff. */
.kk-boot-splash-reload {
    position: absolute;
    bottom: 48px;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--et-color-accent, #7A4E7A);
    text-decoration: underline;
    opacity: 0;
    animation: kk-splash-reveal 0.4s ease 12s forwards;
}

@keyframes kk-splash-pulse {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.06); }
}

@keyframes kk-splash-dot {
    0%, 100% { opacity: 0.35; transform: translateY(0); }
    50%      { opacity: 1;    transform: translateY(-3px); }
}

@keyframes kk-splash-reveal {
    to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .kk-boot-splash-logo { animation: none; }
    .kk-boot-splash-dots span { animation: none; opacity: 0.7; }
    /* The reload reveal stays: it is a one-shot opacity fade, and hiding the
       escape hatch would trade motion comfort for a dead end. */
}

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

/* The two spacing utilities that survived the MudBlazor removal. They used to
   come from MudBlazor.min.css (4 = 16px on its 4px scale); only these two
   still have call sites (EtSkeleton stacks, .nac-wrap), so they are declared
   here rather than reviving a whole utility layer. */
.mt-4 { margin-top: var(--et-space-4); }
.mb-4 { margin-bottom: var(--et-space-4); }

.bordered-row {
    border-bottom: 1px solid var(--et-color-border);
}

.bordered-row:last-child {
    border-bottom: none;
}

.skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    background: var(--et-color-accent);
    color: var(--et-cta-ink);
    padding: 8px;
    z-index: 100;
}

.skip-link:focus { top: 0; }

/* ──────────── Aesthetic baseline ──────────── */

/* Lowercase mono status chip — applied via class on chip span */
.et-status-chip {
    display: inline-flex;
    align-items: center;
    padding: 2px 10px;
    font-family: var(--et-font-mono);
    font-size: 0.6875rem;
    font-weight: 500;
    line-height: 0.875rem;
    letter-spacing: 0.2px;
    text-transform: lowercase;
    border-radius: var(--et-radius-pill);
    white-space: nowrap;
}

/* Main content default padding. No top padding: the header is in flow
   (sticky) at every width now, so there is nothing to clear. The value used
   to be --et-appbar-height, which cleared the fixed MudAppBar on a phone and
   was cancelled with !important on desktop. */
/* `position: relative` is load-bearing for the pull-to-refresh gesture, which
   offsets this element with `top` (etPullRefresh in app-init.js) rather than
   `transform`. A transformed ancestor becomes the containing block for its
   `position: fixed` descendants, and the pages render their .et-fab INSIDE
   this element — so a transform here detaches the "Új költség" pill from the
   viewport and drops it to the bottom edge of a box that is min-height: 100vh
   BELOW a sticky header. Relative positioning offsets the subtree without
   capturing anything fixed. Same reasoning as .et-panel-outlet, which animates
   `right` instead of a transform for the identical reason. */
.main-content {
    position: relative;
    padding: 0 var(--et-space-6) var(--et-space-6);
    min-height: 100vh;
}

/* ──────────── Pull-to-refresh ring (MainLayout + etPullRefresh) ────────────
   Owner feedback 2026-09-20: the gesture used to fire mid-drag with nothing
   drawn, so a refresh arrived as a sudden skeleton swap. This is the ring the
   pull now reveals; app-init.js owns every dynamic value on it (opacity, the
   rotation custom property, the two state classes) and translates
   .main-content down under the sticky header at the same time.

   Here rather than in ds.css on purpose: ds.css is one block per component
   under Components/Ds, and this is not a component — it is a single chrome
   element MainLayout renders next to the header, with nothing to preview in
   the DS gallery and no C# behind it.

   Fixed, not in flow: the element exists on EVERY screen, including the ones
   that never attach the gesture, so it must cost no layout anywhere. Top is
   --et-appbar-height (the same token the sticky group tabs clear the header
   with) plus a gap. z-index 1101 = under .app-topbar (1102) so the ring can
   tuck back under the header as the content unwinds. */
.et-ptr {
    position: fixed;
    top: calc(var(--et-appbar-height) + 8px);
    left: 50%;
    z-index: 1101;
    width: 24px;
    height: 24px;
    box-sizing: border-box;
    border-radius: 50%;
    border: 2px solid var(--et-color-border);
    border-top-color: var(--et-color-accent);
    background: var(--et-color-surface);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateX(-50%) rotate(var(--et-ptr-rotate, 0deg));
}
/* Only a live pull makes it paintable at all — without this the ring would sit
   in every screenshot and every static dump at opacity 0 but still composited. */
.et-ptr--active { visibility: visible; }
/* The keyframes restate translateX(-50%) because an animation on `transform`
   replaces the whole property, centering included. */
.et-ptr--spinning { animation: et-ptr-spin .8s linear infinite; }
@keyframes et-ptr-spin {
    from { transform: translateX(-50%) rotate(0deg); }
    to   { transform: translateX(-50%) rotate(360deg); }
}
/* A band that is the screen's header holds still during a pull: etPullRefresh
   gives it the opposite `top` of .main-content's, which needs a positioned box.
   The page band and the illustrated or photo group band already are one; the
   plain group band in the native shell is the case this covers. */
.main-content .et-group-view-header { position: relative; }
/* Reduced motion keeps the ring — it is the only "working" signal on screen —
   and drops the spin, matching .et-attach-thumb-spinner's intent. */
@media (prefers-reduced-motion: reduce) {
    .et-ptr--spinning { animation: none; }
}

/* ErrorBoundary fallback card (MainLayout). Rendered inside an EtSurface, so
   only the type + the action row live here. */
.et-error-boundary { margin: var(--et-space-6) 0; }
.et-error-boundary-title {
    font-size: 1.0625rem;   /* 17px */
    font-weight: 700;
    color: var(--et-color-ink);
    margin-bottom: var(--et-space-2);
}
.et-error-boundary-body {
    font-size: 0.875rem;    /* 14px */
    color: var(--et-color-ink-2);
    margin-bottom: var(--et-space-4);
}
.et-error-boundary-actions {
    display: flex;
    gap: var(--et-space-2);
}

/* ── PWA install guide (iOS) dialog body ────────────────────────────
   Numbered steps under the intro line; the intro reuses the shared
   .et-expense-detail-dialog-text. */
.et-pwa-guide {
    margin: 0;
    padding-left: 20px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.et-pwa-guide-step {
    font-size: 13px;
    line-height: 1.45;
    color: var(--et-color-ink-2);
}

/* ──────────── Panel outlet (docked side-panel rail) ────────────
   Components/Common/PanelOutlet.razor renders here — a fixed 460px rail on
   the right edge that hosts EtPanel in docked mode. See
   docs/superpowers/specs/2026-08-02-panel-outlet-design.md §2/§4.

   The slide animates `right`, NOT `transform`, on purpose: a transformed
   ancestor becomes the containing block for `position: fixed` descendants,
   which would trap the panels' own `.et-modal-overlay` dialogs and
   `.et-dropdown-backdrop` hit areas inside the 460px rail. Animating `right`
   on a single element costs nothing here and keeps those overlays viewport-
   sized. Same 220ms curve either way.

   z-index sits above the sticky .app-topbar (1102) / .et-fab (1101) and below
   the non-docked .et-panel (1400) and .et-modal-overlay (1500), so a
   dialog opened from inside the panel still covers it. */
.et-panel-outlet {
    position: fixed;
    top: 0;
    bottom: 0;
    right: -460px;
    width: 460px;
    max-width: 100vw;
    z-index: 1350;
    transition: right 0.22s ease;
    pointer-events: none;
}

.et-panel-outlet--open {
    right: 0;
    pointer-events: auto;
}

/* The outlet's overlay form (below 960px) wraps the hosted page in this, only
   so et:motion can tell that page's EtPanel from any other sheet (app-init.js,
   hostedPanel). No box at all: the fixed panel inside positions against the
   viewport exactly as it did when it had no wrapper. */
.et-panel-host { display: contents; }

/* Explicit for readability — the rail is already parked off-screen by the
   base rule, so the close transition runs from wherever it is. */
.et-panel-outlet--closing {
    right: -460px;
    pointer-events: none;
}

/* The rail hugs the VIEWPORT's right edge, full height — and instead of
   squeezing the content inside the card, the whole centered .app-shell
   card slides LEFT to make room (user decision, 2026-08-02): overriding
   margin-right while margin-left stays auto re-solves the centering within
   the remaining width, so card + panel together use the full screen. The
   margin transition matches the rail's 220ms slide. */
@media (min-width: 960px) {
    body > #app > .app-shell,
    body > #app .app-shell {
        transition: margin 0.22s ease;
    }
    body > #app > .app-shell.et-panel-open,
    body > #app .app-shell.et-panel-open {
        margin-right: calc(460px + 24px);
    }
}

/* Below ~1100px the shift would squeeze the card past usefulness — let the
   rail overlay the page instead. */
@media (max-width: 1100px) {
    body > #app > .app-shell.et-panel-open,
    body > #app .app-shell.et-panel-open {
        margin-right: auto;
    }
}

/* ──────────── Floating action pill (Components/Ds/EtFab) ────────────
   The FAB is the phone's primary action; from 960px up the header CTA is,
   so the pill goes away. The atom itself carries no viewport query (that
   would blank the DS gallery, which runs at desktop width), and the gallery
   specimens opt out here through .et-fab--inline.

   The bottom padding is the page's clearance for the pill, which floats over
   the content — it replaces the BottomNav compensation that used to sit in
   the phone block below, and runs to the same 960px boundary the pill does.

   Derived from --et-fab-bottom rather than restating a number: 44px is the
   pill's own height, so height + its offset + a gap. In a browser that is the
   66px + safe-area this replaces, and in an installed PWA it grows with the
   pill instead of letting it drift over the last row. */
@media (max-width: 959.98px) {
    .main-content {
        padding-bottom: calc(44px + var(--et-fab-bottom) + var(--et-space-4));
    }
}
@media (min-width: 960px) {
    .et-fab:not(.et-fab--inline) { display: none; }
}

/* ──────────── Mobile media block (< md) ──────────── */
@media (max-width: 767.98px) {
    .main-content {
        padding-left: var(--et-space-4);
        padding-right: var(--et-space-4);
    }

    /* Settle-up / form papers — let them fill the viewport on phones */
    .mobile-fluid {
        max-width: 100% !important;
        width: 100% !important;
    }


}

/* Flex row helpers: let a label/name shrink and ellipsis-truncate instead of
   wrapping or pushing siblings onto a second line (split rows, breakdown rows). */
.et-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.et-shrink   { min-width: 0; flex: 1 1 auto; }
.et-noshrink { flex: 0 0 auto; }

/* No page zoom from a touch gesture — INSIDE THE INSTALLED APP ONLY, hence
   the .et-standalone gate (app-init.js stamps it for an installed PWA or the
   native shell). `pan-x pan-y` is the whole declaration: it keeps every
   scroll/swipe the app relies on (page scroll, the card shelves, the sheet
   drag) and drops exactly the two gestures that aren't ours — double-tap-to-
   zoom and pinch-zoom, both of which are only enabled under `auto` /
   `manipulation`. Declared on html so it inherits down the whole tree; a
   descendant that needs a different value can still override.

   Companions on the same gate: the viewport meta keys appended in app-init.js
   (Android's half) and the gesture* handlers next to them (iOS Safari's, which
   honours touch-action only for a gesture that STARTS inside the element).
   All three tighten together or not at all. */
html.et-standalone {
    touch-action: pan-x pan-y;
}

/* In a browser, `manipulation` instead — the middle value, and it is exactly
   the line we want: double-tap-to-zoom stays off (that one is a tap that went
   wrong, never an intent), while pinch goes back to the browser. Pinch there
   is not only page zoom: it is how Safari opens the TAB OVERVIEW, and
   swallowing it left iPhone users unable to reach their own tabs (reported
   2026-09-11). It is also how a reader zooms, which Apple protects on purpose.
   The two gestures cannot be told apart, so the split is by context, not by
   gesture: the installed app above, the browser here. */
html:not(.et-standalone) {
    touch-action: manipulation;
}

/* Prevent stray text selection / long-press cursor-in-label when tapping
   non-input UI on mobile. Re-enable selection on inputs + content-
   editable + intentionally selectable code blocks. */
body {
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    user-select: none;
}
input,
textarea,
[contenteditable="true"],
code,
pre {
    -webkit-user-select: text;
    user-select: text;
}

/* ──────── Header form with no store pill (.et-no-store-pill) ────────
   The block above shrinks the switcher to clear room for the "App letöltése"
   store pill, and .app-topbar's phone block hides the CTA. Where the pill does
   not render, the header pays rent for two absent controls: a group pill
   squeezed to a disc for no reason, and an empty .et-topbar-right still
   claiming the topbar's flex gap and holding the bell and the avatar off the
   trailing edge.

   The gate is the pill's OWN predicate, stamped by app-init.js — not the
   viewport, which is what #319 keyed on and got wrong. It covers every case
   where etStore.platform answers null: the installed PWA, the native shell,
   AND a desktop browser narrowed below 960px (a laptop has no app store to
   send anyone to, however narrow the window). #331 fixed the first two by
   keying on .et-standalone; that was the same mistake one size smaller.

   These rules restate the pre-#319 header, bounded by the same 959.98px edge
   as the branch they undo: at >=960px the header is already the original one
   and .et-topbar-right holds the real CTA there.

   This IS the header the landing screenshots show — tools/screens/shoot-
   landing.ps1 used to carry a hand-written copy of these rules, which
   CLAUDE.md flagged as an unenforced mirror. It now stamps the class on the
   dump instead, so this block is the single source. */
@media (max-width: 959.98px) {
    html.et-no-store-pill .app-topbar .et-group-switcher-label {
        display: inline;
        /* The clamp #319 retired with the label: a long group name must not
           push the bell and the avatar off the trailing edge. */
        max-width: 40vw;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    html.et-no-store-pill .app-topbar .et-group-switcher { min-height: 0; }
    html.et-no-store-pill .app-topbar .et-group-switcher--compact {
        padding: 6px 12px 6px 8px;
        gap: 8px;
    }
    html.et-no-store-pill .app-topbar .et-group-switcher-icon { display: none; }
    html.et-no-store-pill .app-topbar .et-group-switcher--compact .et-group-switcher-dots {
        display: inline-flex;
    }

    /* Empty container, so it goes rather than sits: its margin-left:auto is
       what pushed the trailing group to the edge, and the bell inherits that
       job. Without this the avatar stops 10px short of the edge. */
    html.et-no-store-pill .app-topbar .et-topbar-right { display: none; }
    html.et-no-store-pill .app-topbar .et-notif-dropdown { margin-left: auto; }
    /* Restores the desktop bell↔avatar gap the phone block zeroes — with the
       store pill gone there is room for it again. */
    html.et-no-store-pill .app-topbar .et-appbar-avatar { margin-left: 6px; }
}

/* Belt and braces: StoreLinkPill renders nothing when etStore.platform answers
   null, which is exactly when this class is stamped, so the app can't reach
   this rule. It earns its place by making the class a complete description of
   the pill-less header — the ScreenDumps harness answers 'android' for every
   dump, so the pill IS in the HTML the landing shoots, and by a future render
   path that forgets the JS gate. */
html.et-no-store-pill .app-topbar .et-appbar-store { display: none; }

/* ──────────── AdaptiveCardStrip (.et-strip) ──────────────
   Horizontal collection shell used by Home's two carousels (and other
   adaptive grids) — emitted by Components/Ui/AdaptiveCardStrip.razor with
   a `data-strip-count` attribute that drives the layout switch:

     - 1/2/3 (desktop) or 1 (mobile)   → grid, cards stretch edge-to-edge.
     - 4+ (desktop) or 2+ (mobile)     → scroll-snap horizontal carousel.

   The default state is the carousel (works regardless of count), and the
   grid overrides apply only when count is low enough to fit without
   horizontal overflow. Children render whatever cards the caller passes —
   the shell controls only the container chrome. */
.et-strip {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
}
.et-strip > * {
    flex: 0 0 auto;
    scroll-snap-align: start;
}

/* Desktop: grid up to 3 items. Above MD breakpoint matches MudHidden's
   SmAndDown / MdAndUp split used elsewhere. */
@media (min-width: 960px) {
    .et-strip[data-strip-count="1"],
    .et-strip[data-strip-count="2"],
    .et-strip[data-strip-count="3"] {
        display: grid;
        overflow-x: visible;
        overflow-y: visible;
        scroll-snap-type: none;
        /* The strip classes (.nac-strip / .uec-carousel) carry 4px/8px of
           padding as shadow breathing room inside their overflow container.
           Grid mode has no overflow clipping, so the sides only made the
           cards read narrower than the full-width sections below them — and
           the top pushed the first card 4px below the EtSurface starting the
           column beside it, which is the visible half of "Tennivaló and
           Akiknek tartozol don't line up". */
        padding-left: 0;
        padding-right: 0;
        padding-top: 0;
    }
    .et-strip[data-strip-count="1"] { grid-template-columns: 1fr; }
    .et-strip[data-strip-count="2"] { grid-template-columns: 1fr 1fr; }
    .et-strip[data-strip-count="3"] { grid-template-columns: 1fr 1fr 1fr; }
    .et-strip[data-strip-count="1"] > *,
    .et-strip[data-strip-count="2"] > *,
    .et-strip[data-strip-count="3"] > * {
        flex: 1 1 auto;
        min-width: 0;
        width: auto;
        scroll-snap-align: none;
    }
}

/* Mobile: grid only for count = 1; 2+ stays as a scroll carousel. */
@media (max-width: 959.98px) {
    .et-strip[data-strip-count="1"] {
        display: grid;
        grid-template-columns: 1fr;
        overflow-x: visible;
        overflow-y: visible;
        scroll-snap-type: none;
        padding-left: 0;  /* same rationale as the desktop grid mode */
        padding-right: 0;
        padding-top: 0;
    }
    .et-strip[data-strip-count="1"] > * {
        flex: 1 1 auto;
        min-width: 0;
        width: auto;
        scroll-snap-align: none;
    }
}

/* Dot row under a one-card-per-page strip (mobile compaction §2.3). Razor
   renders N dots with the first active; carousel.js's update() moves the
   is-active class as the strip scrolls (rounded scrollLeft / clientWidth),
   reaching the row as the strip's nextElementSibling. Desktop never shows
   it — there the strip is a grid or an arrow carousel. */
.et-strip-dots { display: none; }
@media (max-width: 959.98px) {
    .et-strip-dots {
        display: flex;
        justify-content: center;
        gap: 6px;
        margin-top: 8px;
    }
    .et-strip-dot {
        width: 6px;
        height: 6px;
        border-radius: 50%;
        background: var(--et-color-ink-3);
        transition: background-color .15s;
    }
    .et-strip-dot.is-active { background: var(--et-color-accent); }
}

/* Hide carousel arrows when the wrap holds a grid-mode strip (modern
   browsers — Safari 15.4+, Chrome 105+). Falls back to a no-op on older
   browsers, where the arrows just don't fire any scroll action. */
@media (min-width: 960px) {
    .nac-strip-wrap:has(.et-strip[data-strip-count="1"]) .nac-arrow,
    .nac-strip-wrap:has(.et-strip[data-strip-count="2"]) .nac-arrow,
    .nac-strip-wrap:has(.et-strip[data-strip-count="3"]) .nac-arrow { display: none; }
}
@media (max-width: 959.98px) {
    .nac-strip-wrap:has(.et-strip[data-strip-count="1"]) .nac-arrow { display: none; }
}

/* ──────────── Page-level empty state hero ──────────── */
/* Larger surface w/ tinted icon circle for "no groups yet", "no upcoming
   events", and other page-wide empty surfaces. Inline list-empty states
   (compact "Minden rendben" strips) intentionally stay tiny. */
.et-empty-state {
    background: color-mix(in srgb, var(--et-color-surface) 65%, var(--et-color-bg));
}
.et-empty-state-icon-wrap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--et-color-accent) 12%, var(--et-color-surface-2));
}

/* ──────────── Subtle page background pattern ──────────── */
/* THE PLAIN BACKGROUND IS THE DEFAULT, and it is the default by being the
   absence of a rule: a body with no [data-bg-pattern] (or with "none", which
   no block below matches) simply keeps its base colour. Nothing has to switch
   the decoration off, so no layer — CSS, JS or C# — needs to name the default.

   The two opt-in variants each carry their own radial-dot grid as a second
   background layer, so decoration is entirely additive:

     "abstract" — diamond, 4-pt sparkle, dot cluster, hexagon, ring.
                  Geometric, modern, restrained; opt-in via profile.
     "themed"   — flower, gift box, cake, small heart, balloon.
                  Warmer and more representational; opt-in via profile.

   ("none" is the profile's "Sima" option, and replaces the old escape hatch
   [data-card-style="classic"] that the removed CardChrome toggle drove.)

   SVG colors are hardcoded (#A89880 light, #D6CBB8 dark) so a single tile
   works across all palettes without color-mix on SVG attrs. */

/* ── Abstract pattern: geometric shapes with concentric detail ──
   Diamond + inner diamond, sparkle + center dot (rotated 15°), dot cluster,
   hexagon + inner hexagon + center dot (rotated 8°), double-ring + center
   dot. Subtle off-axis rotation on some shapes breaks the "rubber-stamp
   regularity" of the tile. */
[data-bg-pattern="abstract"] body {
    background-image:
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><g fill='%23A89880' fill-opacity='0.14'><path d='M45 28 L62 45 L45 62 L28 45 Z'/><path d='M135 48 L139 62 L153 66 L139 70 L135 84 L131 70 L117 66 L131 62 Z' transform='rotate(15 135 66)'/><circle cx='195' cy='42' r='3'/><circle cx='208' cy='48' r='2'/><circle cx='200' cy='58' r='2'/><path d='M170 158 L186 168 L186 188 L170 198 L154 188 L154 168 Z' transform='rotate(8 170 178)'/></g><g fill='none' stroke='%238B7355'><path d='M45 36 L54 45 L45 54 L36 45 Z' stroke-opacity='0.28' stroke-width='0.8'/><path d='M170 168 L181 174 L181 184 L170 190 L159 184 L159 174 Z' transform='rotate(8 170 178)' stroke-opacity='0.28' stroke-width='0.8'/><circle cx='170' cy='179' r='1.6' fill='%238B7355' fill-opacity='0.3' stroke='none'/><circle cx='60' cy='170' r='13' stroke-opacity='0.22' stroke-width='2'/><circle cx='60' cy='170' r='7' stroke-opacity='0.3' stroke-width='0.8'/><circle cx='60' cy='170' r='1.3' fill='%238B7355' fill-opacity='0.32' stroke='none'/><circle cx='135' cy='66' r='1.4' fill='%238B7355' fill-opacity='0.3' stroke='none'/></g></svg>"),
        radial-gradient(
            color-mix(in srgb, var(--et-color-card-stripe) 9%, transparent) 1.4px,
            transparent 1.4px
        );
    background-size: 240px 240px, 22px 22px;
}
[data-theme="dark"][data-bg-pattern="abstract"] body {
    background-image:
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><g fill='%23D6CBB8' fill-opacity='0.13'><path d='M45 28 L62 45 L45 62 L28 45 Z'/><path d='M135 48 L139 62 L153 66 L139 70 L135 84 L131 70 L117 66 L131 62 Z' transform='rotate(15 135 66)'/><circle cx='195' cy='42' r='3'/><circle cx='208' cy='48' r='2'/><circle cx='200' cy='58' r='2'/><path d='M170 158 L186 168 L186 188 L170 198 L154 188 L154 168 Z' transform='rotate(8 170 178)'/></g><g fill='none' stroke='%23A89880'><path d='M45 36 L54 45 L45 54 L36 45 Z' stroke-opacity='0.32' stroke-width='0.8'/><path d='M170 168 L181 174 L181 184 L170 190 L159 184 L159 174 Z' transform='rotate(8 170 178)' stroke-opacity='0.32' stroke-width='0.8'/><circle cx='170' cy='179' r='1.6' fill='%23A89880' fill-opacity='0.35' stroke='none'/><circle cx='60' cy='170' r='13' stroke-opacity='0.25' stroke-width='2'/><circle cx='60' cy='170' r='7' stroke-opacity='0.32' stroke-width='0.8'/><circle cx='60' cy='170' r='1.3' fill='%23A89880' fill-opacity='0.36' stroke='none'/><circle cx='135' cy='66' r='1.4' fill='%23A89880' fill-opacity='0.32' stroke='none'/></g></svg>"),
        radial-gradient(
            color-mix(in srgb, var(--et-color-card-stripe) 18%, transparent) 1.4px,
            transparent 1.4px
        );
}

/* ── Themed pattern: representational motifs with crafted detail ──
   Each motif slightly rotated off-axis so the tile doesn't read as
   stamped, and bodies get an explicit edge stroke to define silhouette.

   Flower (rot 12°):  5 ellipse petals rotated 72° apart + tinted center
   Gift   (rot -8°):  outlined box body + crossing ribbons + bow loops +
                      knot + 2 dangling ribbon tails
   Cake   (rot -5°):  two stacked tiers (filled bodies + edge stroke) on an
                      outlined plate, scalloped icing tops, piped border
                      dots on bottom tier, 2 candles with teardrop flames
   Heart  (rot -10°): outlined body + nested inner heart (no fill)
   Balloon(rot -15°): outlined body + knot triangle + curved string +
                      shading crescent
   Detail strokes/fills use %238B7355 (cream's card-stripe taupe) so they
   read as darker veining over the lighter %23A89880 base. */
[data-bg-pattern="themed"] body {
    background-image:
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><g fill='%23A89880' fill-opacity='0.14'><g transform='translate(50,45) rotate(12)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/><g transform='rotate(72)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><g transform='rotate(144)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><g transform='rotate(216)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><g transform='rotate(288)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><circle cx='0' cy='0' r='2.4' fill='%238B7355' fill-opacity='0.32'/></g><g transform='translate(135,72) rotate(-8)'><rect x='-9' y='-1' width='18' height='13' stroke='%238B7355' stroke-opacity='0.32' stroke-width='0.5'/><rect x='-9' y='3' width='18' height='2.5' fill='%238B7355' fill-opacity='0.3'/><rect x='-1' y='-1' width='2' height='13' fill='%238B7355' fill-opacity='0.3'/><ellipse cx='-3.5' cy='-3.5' rx='3.2' ry='1.8' fill='%238B7355' fill-opacity='0.3'/><ellipse cx='3.5' cy='-3.5' rx='3.2' ry='1.8' fill='%238B7355' fill-opacity='0.3'/><circle cx='0' cy='-3.5' r='1.3' fill='%238B7355' fill-opacity='0.35'/><path d='M-1.4 -2 L-2.5 1 L-1.5 1 L-0.3 -2 Z' fill='%238B7355' fill-opacity='0.3'/><path d='M1.4 -2 L2.5 1 L1.5 1 L0.3 -2 Z' fill='%238B7355' fill-opacity='0.3'/></g><g transform='translate(200,52) rotate(-5)'><ellipse cx='0' cy='12' rx='10' ry='1.5' stroke='%238B7355' stroke-opacity='0.32' stroke-width='0.5' fill='none'/><rect x='-8' y='4' width='16' height='8' stroke='%238B7355' stroke-opacity='0.32' stroke-width='0.5'/><path d='M-8 4 Q-6 2 -4 4 Q-2 2 0 4 Q2 2 4 4 Q6 2 8 4' stroke='%238B7355' stroke-opacity='0.3' stroke-width='0.5' fill='none'/><circle cx='-5' cy='8' r='0.6' fill='%238B7355' fill-opacity='0.32'/><circle cx='0' cy='8' r='0.6' fill='%238B7355' fill-opacity='0.32'/><circle cx='5' cy='8' r='0.6' fill='%238B7355' fill-opacity='0.32'/><rect x='-5' y='-3' width='10' height='7' stroke='%238B7355' stroke-opacity='0.32' stroke-width='0.5'/><path d='M-5 -3 Q-3.5 -4.5 -2 -3 Q0 -4.5 2 -3 Q3.5 -4.5 5 -3' stroke='%238B7355' stroke-opacity='0.3' stroke-width='0.5' fill='none'/><rect x='-2.5' y='-9' width='1' height='6' fill='%238B7355' fill-opacity='0.32'/><rect x='1.5' y='-9' width='1' height='6' fill='%238B7355' fill-opacity='0.32'/><path d='M-2 -9 Q-3 -10.5 -2 -12 Q-1 -10.5 -2 -9 Z' fill='%238B7355' fill-opacity='0.35'/><path d='M2 -9 Q1 -10.5 2 -12 Q3 -10.5 2 -9 Z' fill='%238B7355' fill-opacity='0.35'/></g><g transform='translate(70,175) rotate(-10)'><path d='M0 0 C-4 -5 -10 -3 -9 3 C-8 7 0 13 0 13 C0 13 8 7 9 3 C10 -3 4 -5 0 0 Z' stroke='%238B7355' stroke-opacity='0.34' stroke-width='0.6'/><path d='M0 3 C-2.5 0 -6 2 -5.5 5 C-5 7.5 0 11 0 11 C0 11 5 7.5 5.5 5 C6 2 2.5 0 0 3 Z' stroke='%238B7355' stroke-opacity='0.3' stroke-width='0.4' fill='none'/></g><g transform='translate(175,175) rotate(-15)'><ellipse cx='0' cy='-3' rx='5' ry='6.5' stroke='%238B7355' stroke-opacity='0.32' stroke-width='0.5'/><path d='M-1.5 3 L1.5 3 L0 5 Z' fill='%238B7355' fill-opacity='0.35'/><path d='M0 5 Q-2 9 0 13' stroke='%238B7355' stroke-opacity='0.32' stroke-width='0.6' fill='none'/><path d='M-3 -6 Q-4 -4 -3.5 -1' stroke='%238B7355' stroke-opacity='0.28' stroke-width='0.4' fill='none'/></g></g></svg>"),
        radial-gradient(
            color-mix(in srgb, var(--et-color-card-stripe) 9%, transparent) 1.4px,
            transparent 1.4px
        );
    background-size: 240px 240px, 22px 22px;
}
[data-theme="dark"][data-bg-pattern="themed"] body {
    background-image:
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><g fill='%23D6CBB8' fill-opacity='0.13'><g transform='translate(50,45) rotate(12)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/><g transform='rotate(72)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><g transform='rotate(144)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><g transform='rotate(216)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><g transform='rotate(288)'><ellipse cx='0' cy='-7' rx='3.5' ry='5.5'/></g><circle cx='0' cy='0' r='2.4' fill='%23A89880' fill-opacity='0.36'/></g><g transform='translate(135,72) rotate(-8)'><rect x='-9' y='-1' width='18' height='13' stroke='%23A89880' stroke-opacity='0.36' stroke-width='0.5'/><rect x='-9' y='3' width='18' height='2.5' fill='%23A89880' fill-opacity='0.34'/><rect x='-1' y='-1' width='2' height='13' fill='%23A89880' fill-opacity='0.34'/><ellipse cx='-3.5' cy='-3.5' rx='3.2' ry='1.8' fill='%23A89880' fill-opacity='0.34'/><ellipse cx='3.5' cy='-3.5' rx='3.2' ry='1.8' fill='%23A89880' fill-opacity='0.34'/><circle cx='0' cy='-3.5' r='1.3' fill='%23A89880' fill-opacity='0.4'/><path d='M-1.4 -2 L-2.5 1 L-1.5 1 L-0.3 -2 Z' fill='%23A89880' fill-opacity='0.34'/><path d='M1.4 -2 L2.5 1 L1.5 1 L0.3 -2 Z' fill='%23A89880' fill-opacity='0.34'/></g><g transform='translate(200,52) rotate(-5)'><ellipse cx='0' cy='12' rx='10' ry='1.5' stroke='%23A89880' stroke-opacity='0.36' stroke-width='0.5' fill='none'/><rect x='-8' y='4' width='16' height='8' stroke='%23A89880' stroke-opacity='0.36' stroke-width='0.5'/><path d='M-8 4 Q-6 2 -4 4 Q-2 2 0 4 Q2 2 4 4 Q6 2 8 4' stroke='%23A89880' stroke-opacity='0.34' stroke-width='0.5' fill='none'/><circle cx='-5' cy='8' r='0.6' fill='%23A89880' fill-opacity='0.36'/><circle cx='0' cy='8' r='0.6' fill='%23A89880' fill-opacity='0.36'/><circle cx='5' cy='8' r='0.6' fill='%23A89880' fill-opacity='0.36'/><rect x='-5' y='-3' width='10' height='7' stroke='%23A89880' stroke-opacity='0.36' stroke-width='0.5'/><path d='M-5 -3 Q-3.5 -4.5 -2 -3 Q0 -4.5 2 -3 Q3.5 -4.5 5 -3' stroke='%23A89880' stroke-opacity='0.34' stroke-width='0.5' fill='none'/><rect x='-2.5' y='-9' width='1' height='6' fill='%23A89880' fill-opacity='0.36'/><rect x='1.5' y='-9' width='1' height='6' fill='%23A89880' fill-opacity='0.36'/><path d='M-2 -9 Q-3 -10.5 -2 -12 Q-1 -10.5 -2 -9 Z' fill='%23A89880' fill-opacity='0.4'/><path d='M2 -9 Q1 -10.5 2 -12 Q3 -10.5 2 -9 Z' fill='%23A89880' fill-opacity='0.4'/></g><g transform='translate(70,175) rotate(-10)'><path d='M0 0 C-4 -5 -10 -3 -9 3 C-8 7 0 13 0 13 C0 13 8 7 9 3 C10 -3 4 -5 0 0 Z' stroke='%23A89880' stroke-opacity='0.38' stroke-width='0.6'/><path d='M0 3 C-2.5 0 -6 2 -5.5 5 C-5 7.5 0 11 0 11 C0 11 5 7.5 5.5 5 C6 2 2.5 0 0 3 Z' stroke='%23A89880' stroke-opacity='0.34' stroke-width='0.4' fill='none'/></g><g transform='translate(175,175) rotate(-15)'><ellipse cx='0' cy='-3' rx='5' ry='6.5' stroke='%23A89880' stroke-opacity='0.36' stroke-width='0.5'/><path d='M-1.5 3 L1.5 3 L0 5 Z' fill='%23A89880' fill-opacity='0.4'/><path d='M0 5 Q-2 9 0 13' stroke='%23A89880' stroke-opacity='0.36' stroke-width='0.6' fill='none'/><path d='M-3 -6 Q-4 -4 -3.5 -1' stroke='%23A89880' stroke-opacity='0.32' stroke-width='0.4' fill='none'/></g></g></svg>"),
        radial-gradient(
            color-mix(in srgb, var(--et-color-card-stripe) 18%, transparent) 1.4px,
            transparent 1.4px
        );
}

/* ── Pattern size scale: applies to the SVG layer only, the dot grid stays
   at 22px so the underlying texture density doesn't change with size. The
   [data-bg-pattern] attribute selector (any value) scopes these rules so
   the bare dot-only body (no SVG layer) doesn't get its dots resized. */
[data-bg-pattern][data-bg-size="small"] body { background-size: 160px 160px, 22px 22px; }
[data-bg-pattern][data-bg-size="large"] body { background-size: 360px 360px, 22px 22px; }
[data-bg-pattern][data-bg-size="huge"]  body { background-size: 540px 540px, 22px 22px; }

/* ──────────── Sticky bottom CTA (Task 18) ────────────
   Long forms get a CTA bar pinned to the bottom of the viewport on
   mobile so the user doesn't have to scroll back up to hit "Mentés".
   Desktop falls back to inline placement (no stick). */
@media (max-width: 767.98px) {
    .et-sticky-cta {
        position: sticky;
        /* Was BottomNav height + a gap; the nav is gone, so the bar sits at
           the bottom of the viewport with just the gap. */
        bottom: var(--et-space-2);
        z-index: 10;
        margin-top: var(--et-space-4);
        padding: var(--et-space-3);
        background: color-mix(in srgb, var(--et-color-surface) 92%, transparent);
        backdrop-filter: blur(8px);
        -webkit-backdrop-filter: blur(8px);
        border-top: 1px solid var(--et-color-border);
        margin-left: calc(-1 * var(--et-space-4));
        margin-right: calc(-1 * var(--et-space-4));
    }
}

/* Legal pages (.legal-page / .legal-notice etc.) moved to inline <style>
   blocks in wwwroot/landing/adatvedelem.html + felhasznalasi-feltetelek.html.
   The legal pages are now plain static HTML served by render.yaml rewrites
   before the SPA wildcard, so they never load app.css — keeping the rules
   here would just be dead bytes in the Blazor bundle. */

/* ── Bullet list inside an EtInfoBanner (V6 Task 7) ─────────────────
   InviteMemberPanel and the DS catalog's banner specimen. Only the list:
   the banner's summary line and its toggle style themselves from ds.css's
   .et-info-banner-* rules. */
.et-group-new-explainer-list {
    margin: 0;
    padding: 2px 0 0 18px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--et-color-ink-2);
}
.et-group-new-explainer-list li { padding-left: 2px; }

/* ──────────── Shared page hero (Kezdőlap / Költségek / Értesítések /
   Csoportok) ────────────
   One trio for the title block every top-level page opens with. It used to be
   four hand-copied pairs (.et-home-hero-*, .et-expenses-hero-*,
   .et-notifications-hero-*, .et-groups-hero-*) carrying identical values, so
   a type change meant four edits and the copies had already drifted apart in
   their colour tokens.

   Page chrome, not a DS atom: it lives in shell.css because it is the page's
   own opening block, not a reusable component, and ds.css stays the atom
   catalogue.

   Two forms:
     .et-page-hero          — text only (Kezdőlap, Költségek)
     .et-page-hero--split   — text on the left, a single trailing CTA on the
                              right (Értesítések, Csoportok) */
.et-page-hero {
    padding-top: 28px;
    margin-bottom: 22px;
}
/* Generic "back to where I came from" link some full pages render above their
   own .et-page-hero (origin-based — GroupSettings' .et-group-settings-back
   was first; that page has since moved to the band's own back button, but the
   look it set is shared by any other page that needs it, e.g. the expenses
   history page). The sibling rule zeroes the hero's own
   top padding whenever this button actually precedes it, so the two don't
   double up their spacing — pages without the button keep the hero's normal
   28px top. */
.et-page-back {
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;   /* explicit: never inherit a centred button rule */
    gap: 6px;
    background: none;
    border: 0;
    padding: 0;
    margin: 16px 0 10px;
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    color: var(--et-color-ink-2);
    cursor: pointer;
}
.et-page-back:hover { color: var(--et-color-ink); }
.et-page-back + .et-page-hero { padding-top: 0; }
.et-page-hero--split {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}
/* Flex items floor at min-content; without this a long title would refuse to
   shrink and push the trailing CTA out of the row. */
.et-page-hero-text { min-width: 0; }
.et-page-hero-title {
    font-size: 1.75rem;         /* 28px */
    font-weight: 800;
    letter-spacing: -0.03em;
    line-height: 1.15;
    color: var(--et-color-ink);
}
.et-page-hero-subtitle {
    margin-top: 2px;
    font-size: 0.875rem;        /* 14px */
    line-height: 1.35;
    color: var(--et-color-ink-3);
}

/* Phone branch. @media and NOT @container, deliberately: the hero is page
   chrome that spans the whole content column — there is no query container
   anywhere above it (the only containers in this app are DS atom boxes and
   the gallery's specimen columns), so a container query here would never
   fire. Same 767.98px edge as the "Mobile media block (< md)" near the top
   of this file; keep the two in step.

   Values from the mobile canvas (KozosKassza Mobile): 22/800/-0.03em title,
   13/400 subtitle, and the frame's own 18px top / 16px bottom rhythm. Until
   this chunk the heroes did not exist below 960px at all — the phone got its
   title from the AppBar via PageHeaderState — so this branch is new surface,
   not a restyle. */
@media (max-width: 767.98px) {
    .et-page-hero {
        padding-top: 18px;
        margin-bottom: 16px;
    }
    /* The split form's CTA cannot sit beside a 22px title on a 390px screen —
       the Hungarian labels ("Összes olvasottnak jelölése") are longer than the
       space left over. Stack it under the text and let it run full width,
       which is the shape the deleted phone branch built by hand with a
       Block="true" button. */
    .et-page-hero--split {
        flex-direction: column;
        align-items: stretch;
        gap: 12px;
    }
    .et-page-hero-title { font-size: 1.375rem; }   /* 22px */
    .et-page-hero-subtitle { font-size: 0.8125rem; } /* 13px */
}

/* The header size as a UI variant (starter spec D34) — the operator's switch on the Profile
   (UiVariantState stamps html[data-hero-size]; pre-paint-theme.js before the first paint). The
   attribute-free root is the hero above, untouched. These win at every width: html[data-…]
   .et-page-hero is (0,2,1) against (0,1,0) for the base and the phone branch, so one trio per
   pick serves the desktop and the phone alike. The back-link reset is restated for the same
   reason — a variant's padding-top would outrank `.et-page-back + .et-page-hero` (0,2,0). A
   variant comes out whole: retiring hero-size deletes this block with its registry line. */
html[data-hero-size="compact"] .et-page-hero { padding-top: 12px; margin-bottom: 10px; }
html[data-hero-size="compact"] .et-page-hero-title { font-size: 1.25rem; }     /* 20px */
html[data-hero-size="compact"] .et-page-hero-subtitle { font-size: 13px; }
html[data-hero-size="large"] .et-page-hero { padding-top: 36px; margin-bottom: 28px; }
html[data-hero-size="large"] .et-page-hero-title { font-size: 2.25rem; }       /* 36px */
html[data-hero-size="large"] .et-page-hero-subtitle { font-size: 15px; }
html[data-hero-size] .et-page-back + .et-page-hero { padding-top: 0; }

/* ──────────── 404 (/404 + Router NotFound) page chrome ──────────── */
.et-notfound-page {
    max-width: 480px;
    width: 100%;
    margin: 0 auto;
    padding-top: 48px;
}

/* ──────────── Shared page-level error banner spacing ────────────
   EtInfoBanner has no outer margin of its own (it is used inline inside
   form blocks too). Top-of-page error banners on the DS list pages need
   the same 14px rhythm the filter panel uses under it. */
.et-page-error { margin-bottom: 14px; }

/* Installed-client safe area (spike 2026-09-10; widened 2026-09-19 §4.4).
   viewport-fit=cover sits in index.html now, instead of being injected by
   app-init.js for the native shell alone, so EVERY installed client draws
   under the status bar / Dynamic Island — an iOS PWA on the home screen as
   much as the Capacitor WebView. The gate moved from .et-native-shell to
   .et-standalone to follow it: left on the narrower class, an installed PWA
   would have painted its header behind the clock the moment the meta changed.
   .et-standalone is a superset (app-init.js ORs the native flag into it), so
   the native shell keeps exactly the padding it had.

   A browser is excluded on purpose and needs no rule: its own chrome sits
   above the viewport and the top inset reads 0 there. Bottom is already
   covered — --et-fab-bottom carries the bottom inset. */
html.et-standalone .et-topbar {
    /* 12px = the bar's own top padding in ds.css; the inset is added, not
       swapped in. var() before env(): Capacitor injects --safe-area-inset-*
       because Android WebView < 140 answers env() with 0. */
    padding-top: calc(12px + var(--safe-area-inset-top, env(safe-area-inset-top, 0px)));
}

/* ════════════ Phone shell (spec 2026-09-19 §3) ════════════
   Everything below is behind TWO gates at once: `html.et-shell-bar` AND
   max-width 959.98px, the same boundary the FAB and the phone header branch
   use. Desktop (>=960px) keeps the desktop chrome, header CTA and all, in every
   mode — nothing in this section can reach it.

   The class was `html.et-standalone` until 2026-09-22. Two owner decisions
   changed that on the same day:

     1. the shell has to be SWITCHABLE, to be judged on a real phone;
     2. seeing it switched on in a browser, the owner reversed §0/2 and §3's
        "Kapu": the bar goes to the mobile browser too, as the default.

   So the gate is now a derived class rather than the installed-ness predicate:
   app-init.js stamps `.et-shell-bar` from `et:bottom-bar` (localStorage,
   ThemeService), which reads

     always (DEFAULT, incl. missing/unknown) — the shell in every form
     auto                                    — only standalone, the pre-2026-09-22 rule
     never                                   — no shell, even installed

   Only the bar and its companions moved. Being installed still means what it
   meant for everything else: the --et-fab-bottom token (tokens.css), the
   touch-action split further up this file and the store-pill rule inside this
   very section all stay on `.et-standalone`.

   The toggle is a pilot and goes away once the owner settles; this section
   stays, on whichever gate wins.
   ─────────────────────────────────────────────────────────────── */

/* The bar itself. Default OFF — desktop never paints it, and neither does a
   client whose setting is `never`. The bar is hidden by a rule rather than left
   out of the DOM: one selector to read, one place it can be wrong. */
.et-bottom-bar { display: none; }
.et-bottom-bar-backdrop { display: none; }

/* The tab title in the top bar (the greeting on /home), likewise off by
   default. MainLayout renders it only on the four tab roots, so this rule is
   what keeps it out of a desktop header there. */
.et-topbar-title { display: none; }

@media (max-width: 959.98px) {
    /* ── 3.1 Bottom bar ── */
    html.et-shell-bar .et-bottom-bar {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        align-items: end;
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        /* The token is the bar's own chrome and the inset is the strip below
           it, so the two are ADDED by the padding, not by the height as well.
           This element is content-box (the project has no global border-box
           reset — see the note on .nac-strip > .et-todo-card), so the height
           here is content only: carrying the inset in BOTH would have made the
           bar 64 + 2×inset tall, 132px instead of 98px on an iPhone. It read
           correctly until 2026-09-22 only because the term was inert without
           viewport-fit=cover; §4.4 turned it on, so the arithmetic has to be
           right now. Measured in headless Edge with --safe-area-inset-bottom
           forced to 34px: 99px total (64 + 34 + the 1px border), against the
           114px .main-content keeps clear below.

           var() before env(): Capacitor SystemBars injects --safe-area-inset-*
           because Android WebView < 140 answers env() with 0. */
        height: var(--et-bottom-bar-height);
        padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
        /* The page background, not a surface: the reference sits ON the sheet,
           and a raised surface tone under a bg-pattern body reads as a second
           card stuck to the bottom edge. */
        background: var(--et-color-bg);
        border-top: 1px solid var(--et-color-border);
        /* Above the content and at the FAB's old level (1101 as well — the FAB
           is hidden under this gate, so they never compete), below the sticky
           top bar's 1102, the panel outlet (1350), .et-panel (1400) and
           .et-modal-overlay (1500): a sheet or a dialog covers the bar. */
        z-index: 1101;
        /* The centre + overhangs the top edge by 16px. */
        overflow: visible;
        font-family: var(--et-font-family-base);
        /* So the tab-change slide can speak in columns: 20cqw is exactly one
           of the five 1fr columns (the bar has no side padding), whatever the
           phone's width. vw would be off by a scrollbar in a narrowed desktop
           browser. Layout containment makes the bar the containing block of
           anything fixed inside it — which is why the menu's backdrop is a
           sibling of the bar, not a child. */
        container-type: inline-size;
    }
    /* Over the menu's backdrop (1103, which covers the top bar's 1102 too), so
       the tabs and the + stay live while the menu is open. */
    html.et-shell-bar .et-bottom-bar.is-menu-open { z-index: 1104; }

    html.et-shell-bar .et-bottom-bar-tab {
        display: flex;
        flex-direction: column;
        align-items: center;
        /* Explicit, per the DS button rule: never inherit a centred or
           stretched global button alignment. */
        justify-content: flex-start;
        gap: 3px;
        height: var(--et-bottom-bar-height);
        padding: 8px 2px 0;
        border: 0;
        background: none;
        color: var(--et-color-ink-3);
        font: inherit;
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }
    html.et-shell-bar .et-bottom-bar-tab.is-active { color: var(--et-color-accent); }
    html.et-shell-bar .et-bottom-bar-icon { display: block; }
    html.et-shell-bar .et-bottom-bar-label {
        font-size: 11px;
        font-weight: 600;
        line-height: 1.1;
        letter-spacing: -0.01em;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* The active marker. Rendered on every tab and only tinted on the active
       one, so switching tabs cannot change the row's height by a pixel. */
    html.et-shell-bar .et-bottom-bar-rule {
        width: 20px;
        height: 3px;
        margin-top: 2px;
        border-radius: 999px;
        background: transparent;
    }
    html.et-shell-bar .et-bottom-bar-tab.is-active .et-bottom-bar-rule {
        background: var(--et-color-accent);
    }

    /* Tab change (owner pick A, 2026-09-26): the rule slides over from the tab
       that was lit, and the new icon pops. BottomBar stamps .is-arriving on the
       newly lit tab plus --et-bb-from, the signed number of grid columns back
       to the old one, so one keyframe covers every pair of tabs. The old tab's
       rule goes transparent at once and the new one starts from where it sat,
       so there is only ever one rule on screen. The first render stamps
       nothing, so nothing moves on load, and a route that lit no tab gives no
       --et-bb-from — the rule simply appears.

       Gated on et:motion like the rest of #484's motion; the reduced-motion
       blanket in tokens.css shortens both to nothing on its own. */
    html.et-shell-bar:not([data-motion="off"]) .et-bottom-bar-tab.is-arriving .et-bottom-bar-rule {
        animation: et-bb-rule-slide var(--et-motion-enter) var(--et-ease-standard);
    }
    html.et-shell-bar:not([data-motion="off"]) .et-bottom-bar-tab.is-arriving .et-bottom-bar-icon {
        animation: et-bb-icon-pop 250ms cubic-bezier(0.34, 1.56, 0.64, 1);
    }

    /* …and the same slide when the tab change runs inside a page transition
       (et:motion, the section at the end of this file), which is every tab tap
       where View Transitions exist. There the bar is captured as ONE snapshot
       and cross-faded, so a rule sliding in the live bar would be doubled by a
       ghost of itself fading out on the old tab. Instead the lit rule gets a
       name of its own for the transition's length: that takes it out of the
       bar's snapshot, and the transition morphs it from the old tab's
       position to the new one's — the slide, done by the browser. The CSS
       slide above is zeroed meanwhile (on the rule itself, which outranks the
       tab's inline value), or the live rule would slide a second time under
       the morph. It has run its course by the time the attribute goes: it
       starts with the render the transition waits for, and the transition
       outlasts it. The pop stays live — the bar's new snapshot is a live
       picture, so it plays through the cross-fade. */
    html.et-shell-bar[data-page-motion] .et-bottom-bar-tab.is-active .et-bottom-bar-rule {
        view-transition-name: et-bottombar-rule;
    }
    html.et-shell-bar[data-page-motion] .et-bottom-bar-rule { --et-bb-from: 0; }

    html.et-shell-bar .et-bottom-bar-tab:focus-visible {
        outline: 2px solid var(--et-color-accent);
        outline-offset: -2px;
        border-radius: 12px;
    }

    /* Centre slot. The cell keeps its 1fr of the grid; the disc inside it is
       what rises out of the bar. */
    html.et-shell-bar .et-bottom-bar-center {
        /* The anchor of the + menu. */
        position: relative;
        display: flex;
        align-items: flex-start;
        justify-content: center;
        height: var(--et-bottom-bar-height);
    }
    html.et-shell-bar .et-bottom-bar-add {
        position: relative;
        width: 56px;
        height: 56px;
        border: 0;
        border-radius: 50%;
        background: var(--et-cta-gradient);
        color: var(--et-cta-ink);
        display: flex;
        align-items: center;
        justify-content: center;
        /* 16px of the disc sits above the bar's top edge (spec §3.1). A
           transform on this button is safe — nothing position:fixed lives
           inside it — unlike the one on .main-content that had to be
           reverted (#453). */
        transform: translateY(-16px);
        box-shadow: 0 10px 28px rgba(var(--et-cta-shadow-rgb), 0.5);
        cursor: pointer;
        /* translate/scale/opacity: the way back from under a screen's action
           pill (3.1b), on the icon pop's spring, a beat after the pill starts
           sinking so the disc reads as catching it. Individual properties, so
           the press's `transform` below keeps its own quick feel. */
        transition: filter .14s, box-shadow .14s,
                    translate var(--et-motion-enter) var(--et-ease-standard) 150ms,
                    scale var(--et-motion-enter) cubic-bezier(0.34, 1.56, 0.64, 1) 150ms,
                    opacity var(--et-motion-exit) var(--et-ease-enter) 150ms;
    }
    /* Every state restates the lift: `transform` is one property, so a bare
       scale would drop the -16px and the disc would snap into the bar. */
    html.et-shell-bar .et-bottom-bar-add:active {
        filter: brightness(0.94);
        transform: translateY(-16px) scale(0.96);
        box-shadow: 0 6px 18px rgba(var(--et-cta-shadow-rgb), 0.45);
    }
    html.et-shell-bar .et-bottom-bar-add:focus-visible {
        outline: 2px solid var(--et-color-accent);
        outline-offset: 3px;
    }
    /* Drawn, not a font's "+" (see BottomBar), so its box centre IS the
       cross's centre and the flex centring above puts it in the middle of the
       disc with no nudge — and the rotation below turns it on the spot.

       Owner pick D, 2026-09-26: while the + menu is open the + turns 45° into
       a ×, and back when it closes — keyed on the button's own aria-expanded,
       so the look cannot disagree with what a screen reader is told.
       Enter/exit durations, the menu's own pair. */
    html.et-shell-bar .et-bottom-bar-add-glyph {
        display: block;
        transition: transform var(--et-motion-exit) var(--et-ease-standard);
    }
    html.et-shell-bar .et-bottom-bar-add[aria-expanded="true"] .et-bottom-bar-add-glyph {
        transform: rotate(45deg);
        transition-duration: var(--et-motion-enter);
    }

    /* The + menu — the groups list only (owner Q8 = A, 2026-09-26; see
       BottomBar). A small card standing on the disc: centred on it, its
       bottom edge 10px clear of the disc's top (the cell's top edge is the
       bar's, and the disc rises 16px above that). The disc sits in the middle
       column of a full-width bar, i.e. on the viewport's centre line, so
       centring on it plus the max-width is what keeps the card inside 16px
       gutters on any phone.

       Not an EtDropdown, on purpose: below 640px that morphs into a bottom
       sheet, and the owner asked for a popover over the +. The rows are the
       dropdown's rows, though — .et-group-menu-item at the sheet's roomier
       12px, the surface, radius and shadow pair of .et-dropdown-menu.

       Closed is a state, not an absence (the component renders the menu for
       as long as the route is /groups), so the exit can play: it fades, sinks
       8px and shrinks towards the disc (transform-origin bottom centre), and
       only then turns visibility off — which also takes the closed rows out
       of the tab order. Opening is the same path in reverse, on the enter
       pair. */
    html.et-shell-bar .et-bottom-bar-menu {
        position: absolute;
        left: 50%;
        bottom: calc(100% + 26px);
        box-sizing: border-box;
        width: max-content;
        min-width: 200px;
        max-width: calc(100vw - 32px);
        display: flex;
        flex-direction: column;
        gap: 2px;
        padding: 6px;
        background: var(--et-color-surface);
        border-radius: 14px;
        box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.04);
        transform-origin: 50% 100%;
        visibility: hidden;
        opacity: 0;
        transform: translateX(-50%) translateY(8px) scale(0.92);
        transition: opacity var(--et-motion-exit) var(--et-ease-exit),
                    transform var(--et-motion-exit) var(--et-ease-exit),
                    visibility 0s linear var(--et-motion-exit);
    }
    html.et-shell-bar .et-bottom-bar-menu.is-open {
        visibility: visible;
        opacity: 1;
        transform: translateX(-50%);
        transition: opacity var(--et-motion-enter) var(--et-ease-enter),
                    transform var(--et-motion-enter) var(--et-ease-enter),
                    visibility 0s;
    }
    html.et-shell-bar .et-bottom-bar-menu .et-group-menu-item {
        padding: 12px;
        font-size: 14px;
    }
    /* The glyph slot is 16px wide for a text glyph; these are the bar's own
       drawn icons at 18px, so the slot widens to hold them centred. */
    html.et-shell-bar .et-bottom-bar-menu .et-group-menu-glyph {
        width: 20px;
        display: flex;
        justify-content: center;
    }
    html.et-shell-bar .et-bottom-bar-menu .et-group-menu-glyph svg { display: block; }

    /* The outside tap. Transparent, over the page and the top bar, under the
       lifted bar (see .is-menu-open above). */
    html.et-shell-bar .et-bottom-bar-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 1103;
    }

    /* et:motion off: the menu and the glyph change state in place. The
       reduced-motion blanket in tokens.css only shortens durations — it would
       leave the exit's visibility DELAY in, a 200ms window in which the faded
       card still took taps — so reduced motion is spelled out here too. */
    html[data-motion="off"].et-shell-bar .et-bottom-bar-menu,
    html[data-motion="off"].et-shell-bar .et-bottom-bar-add-glyph { transition: none; }
    @media (prefers-reduced-motion: reduce) {
        html.et-shell-bar .et-bottom-bar-menu,
        html.et-shell-bar .et-bottom-bar-add-glyph { transition: none; }
    }

    @keyframes et-bb-rule-slide {
        from { transform: translateX(calc(var(--et-bb-from, 0) * 20cqw)); }
    }
    /* 1 → 1.12 → 1, each leg on a back-out curve, so it overshoots a touch on
       the way up and settles just under 1 on the way down: a spring, not a
       bounce. */
    @keyframes et-bb-icon-pop {
        0%   { transform: scale(1); }
        40%  { transform: scale(1.12); }
        100% { transform: scale(1); }
    }

    /* The morph that slides the rule inside a page transition (see the note
       by .is-arriving above): the CSS slide's own duration and curve, so the
       two paths are one motion. */
    ::view-transition-group(et-bottombar-rule) {
        animation-duration: var(--et-motion-enter);
        animation-timing-function: var(--et-ease-standard);
    }

    /* ── 3.1b A screen's own action (spec 2026-10-01 bottom-bar action pill) ──
       Profil's Mentés, a group settings tab's Új tag / Új esemény / … take the
       centre slot instead of the + (owner, 2026-10-01: "legyen a pluszból egy
       bubival kiemelés fentebb FABként és a navbaron közelebb kerülnek az
       ikonok amikor meg visszamegy plusz helyre akkor mindenki megy a helyére").
       BottomBarAction renders the pill into the slot, and everything here keys
       on the slot holding one (:has), so arriving and leaving are just that
       markup appearing and going.

       Every moving part is a transform or an opacity, so it all runs on the
       compositor: the arriving page renders on the main thread at the same
       moment, and anything layout-driven (a growing width, a folding grid
       track) stalls behind it — on a phone, visibly. (The label's colour is
       the one exception, and only on the way in: a repaint, not a layout.)

       Arriving: the disc floats up and shrinks away while the pill rises out of
       its place as a small bubble, overshoots a touch and settles 10px above
       the bar; its label fades in on the way. The four tabs slide in towards
       the middle into an even four-column spread.
       Leaving: the pill is gone with its screen, so BottomBar renders a
       stand-in (.et-bar-action-ghost) that sinks back down into the disc's
       place while the disc grows back under it and the tabs slide out again —
       the same with or without a page transition. */

    /* The grid stays five 20cqw columns; a four-column spread centres the tabs
       at 12.5 / 37.5 / 62.5 / 87.5cqw instead of 10 / 30 / 70 / 90, so each is
       translated by the difference. Children 1, 2, 4, 5 — 3 is the centre
       slot. Hit-testing follows the translate: a tab is tappable where it is
       drawn. The colour transition stands in for the cross-fade the bar no
       longer gets in a page transition (below). */
    html.et-shell-bar:not([data-motion="off"]) .et-bottom-bar-tab {
        transition: color var(--et-motion-exit),
                    translate var(--et-motion-enter) var(--et-ease-standard);
    }
    html.et-shell-bar .et-bottom-bar:has(.et-bar-action) .et-bottom-bar-tab:nth-child(1) { translate: 2.5cqw 0; }
    html.et-shell-bar .et-bottom-bar:has(.et-bar-action) .et-bottom-bar-tab:nth-child(2) { translate: 7.5cqw 0; }
    html.et-shell-bar .et-bottom-bar:has(.et-bar-action) .et-bottom-bar-tab:nth-child(4) { translate: -7.5cqw 0; }
    html.et-shell-bar .et-bottom-bar:has(.et-bar-action) .et-bottom-bar-tab:nth-child(5) { translate: -2.5cqw 0; }

    /* Out of the tab order and the accessibility tree (visibility, after the
       fade), and out of reach at once (pointer-events) — a tap on the shrinking
       disc must not start an expense. */
    html.et-shell-bar .et-bottom-bar-center:has(.et-bar-action) .et-bottom-bar-add {
        translate: 0 -24px;
        scale: 0.4;
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transition: translate var(--et-motion-exit) var(--et-ease-exit),
                    scale var(--et-motion-exit) var(--et-ease-exit),
                    opacity var(--et-motion-exit) var(--et-ease-exit),
                    visibility 0s linear var(--et-motion-exit);
    }

    /* Centred on the centre cell, i.e. on the bar's middle at any width; its
       bottom 10px above the bar's top edge. `translate` centres it, so
       `transform` stays free for the motion. The exit stand-in wears the same
       pill. */
    html.et-shell-bar :is(.et-bar-action, .et-bar-action-ghost) {
        position: absolute;
        left: 50%;
        bottom: calc(100% + 10px);
        translate: -50% 0;
        box-sizing: border-box;
        height: 48px;
        max-width: calc(100cqw - 32px);
        padding: 0 22px;
        display: flex;
        align-items: center;
        justify-content: center;
        border: 0;
        border-radius: 999px;
        background: var(--et-cta-gradient);
        color: var(--et-cta-ink);
        font: inherit;
        font-size: 15px;
        font-weight: 700;
        white-space: nowrap;
        box-shadow: 0 10px 28px rgba(var(--et-cta-shadow-rgb), 0.5);
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }
    /* The ellipsis needs overflow:hidden, and that clips at the line box — so
       the line box has to hold the Ú's accent and the j's tail, which a
       line-height of 1 does not. */
    html.et-shell-bar .et-bar-action-label {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        line-height: 1.4;
    }
    html.et-shell-bar .et-bar-action:active { filter: brightness(0.94); }
    html.et-shell-bar .et-bar-action:disabled { cursor: default; opacity: 0.8; }
    html.et-shell-bar .et-bar-action:focus-visible {
        outline: 2px solid var(--et-color-accent);
        outline-offset: 3px;
    }

    /* The bubble: from the disc's centre (46px below its own) at 0.35×, one
       spring to its place — a single segment, so it never pauses on the way.
       The text stays clear for the first stretch of the rise and fades in
       over the rest, on the same clock, so it is there when the pill is.
       A new label on a live pill (a group settings tab switch, "Mentés…")
       only slides in, at once — nothing to wait for. */
    html.et-shell-bar:not([data-motion="off"]) .et-bar-action {
        animation: et-bar-action-in 420ms cubic-bezier(0.34, 1.4, 0.64, 1);
    }
    html.et-shell-bar:not([data-motion="off"]) .et-bar-action-label {
        animation: et-bar-action-label-in 160ms var(--et-ease-enter);
    }
    @keyframes et-bar-action-in {
        from { transform: translateY(46px) scale(0.35); opacity: 0; color: transparent; }
        30%  { opacity: 1; }
        35%  { color: transparent; }
    }
    @keyframes et-bar-action-label-in {
        from { opacity: 0; transform: translateY(4px); }
    }

    /* The exit, played by the stand-in: the way in, reversed, on an even
       curve — it eases off its spot and into the disc's, and fades only in
       its second half, so it reads as sinking rather than vanishing. The
       stand-in exists only with motion on, and BottomBar drops it at 450ms —
       keep this duration under that. */
    html.et-shell-bar .et-bar-action-ghost {
        pointer-events: none;
        animation: et-bar-action-out 380ms cubic-bezier(0.4, 0, 0.2, 1) both;
    }
    @keyframes et-bar-action-out {
        50% { opacity: 1; }
        to  { transform: translateY(46px) scale(0.35); opacity: 0; }
    }

    /* In a page transition the bar is the same element on both sides, and what
       changes in it plays live: the rule's own morph, the icon pop, the tabs'
       colour and their slide (above). Cross-fading the two snapshots on top
       would double every tab while it slides, so the old one steps aside at
       once. */
    ::view-transition-old(et-bottombar) { animation: none; opacity: 0; }
    ::view-transition-new(et-bottombar) { animation: none; }

    /* The screen's in-page twin of the action (Profil's Mentés row, the
       settings tabs' header buttons): one action, one place. Same gate as the
       bar itself, so wherever the bar is not shown the twin is. */
    html.et-shell-bar .et-bar-action-twin { display: none; }

    /* The floating pill covers 58px above the bar, so the page owes it that
       much more clearance. */
    html.et-shell-bar:has(.et-bar-action) .main-content {
        padding-bottom: calc(var(--et-bottom-bar-height)
                             + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))
                             + var(--et-space-4) + 58px);
    }

    /* et:motion off: the disc and the pill change in place (the tabs' own
       transition is already gated above). The disc keeps its press feedback. */
    html[data-motion="off"].et-shell-bar .et-bottom-bar-add,
    html[data-motion="off"].et-shell-bar .et-bottom-bar-center:has(.et-bar-action) .et-bottom-bar-add {
        transition: filter .14s, box-shadow .14s;
    }

    /* ── 3.2 FAB and bottom clearance ── */
    /* One plum entry point, and in the installed app it is the bar's centre +.
       --et-fab-bottom stays defined (tokens.css): the DS gallery's inline
       specimen still reads it. */
    html.et-shell-bar .et-fab:not(.et-fab--inline) { display: none; }

    /* The bar floats over the content, so the page owes it clearance. Derived
       from the same token the bar's height comes from, so the two cannot
       drift; the safe-area term is added on both sides for the same reason. */
    html.et-shell-bar .main-content {
        padding-bottom: calc(var(--et-bottom-bar-height)
                             + var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))
                             + var(--et-space-4));
    }

    /* No bar on the invite screen (MainLayout stamps data-route="invite"): the
       invitee is deciding whether to join, and a tab tap there left the invite
       by accident (owner, 2026-10-01). The clearance goes back to .main-content's
       own 24px with it. */
    html.et-shell-bar .app-shell[data-route="invite"] .et-bottom-bar { display: none; }
    html.et-shell-bar .app-shell[data-route="invite"] .main-content { padding-bottom: var(--et-space-6); }

    /* ── 3.3 Header on the tab roots ── */
    /* The group switcher and the avatar menu are both "go somewhere" controls
       whose destinations are tabs now: the list is Csoportok, the profile is
       Profil. Leaving them would give the app two navigations. */
    html.et-shell-bar .app-topbar .et-group-switcher-dropdown,
    html.et-shell-bar .app-topbar .et-appbar-avatar { display: none; }

    /* The tab title takes the space they freed, next to the brand tile — the
       greeting on Kezdőlap, the page title on the other three. The bell is the
       only trailing control left, so it is pushed right here. */
    html.et-shell-bar .et-topbar-title {
        display: block;
        /* Grow into the freed space and shrink past its content: without both,
           a long name pushes the bell off the right edge instead of
           ellipsing. */
        flex: 1 1 auto;
        min-width: 0;
        font-size: 17px;
        font-weight: 700;
        letter-spacing: -0.01em;
        color: var(--et-color-ink);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* Push the bell to the trailing edge. .et-topbar-right already carries a
       margin-left:auto (ds.css), and flexbox splits the free space EVENLY
       between every auto margin in the line — two of them left the bell
       stranded mid-header. Zeroing the other one is what makes this one mean
       "everything from here goes right". */
    html.et-shell-bar .app-topbar .et-notif-dropdown { margin-left: auto; }
    html.et-shell-bar .app-topbar .et-topbar-right { margin-left: 0; }

    /* Belt to StoreLinkPill's own braces: it already self-hides in a native
       shell and an installed PWA (etStore.platform, app-init.js), so this
       changes nothing in the app — but loose JS interop means the pill DOES
       render in a dump, and an "App letöltése" button inside the installed
       app's own header would be a misleading picture to design against.

       The ONE rule in this section that stayed on .et-standalone when the rest
       moved to .et-shell-bar (pilot toggle, 2026-09-22): "can this client be
       sent to a store" is a question about being installed, not about which
       navigation the page wears. A phone browser showing the shell still has an
       app to offer, and hiding the pill there would cost a real install. */
    html.et-standalone .et-appbar-store { display: none; }

    /* Csoportok, Költségek and Profil do the same since 2026-09-25: the title
       is in the header, and the subtitle goes with it (owner pick: hidden, as
       on Kezdőlap — not deleted). The hero box itself stays, empty, so the
       page keeps the same top rhythm under the bar that Kezdőlap has. */
    html.et-shell-bar .et-page-hero--tab .et-page-hero-title,
    html.et-shell-bar .et-page-hero--tab .et-page-hero-subtitle { display: none; }

    /* "← Vissza" on the Activity and Groups roots: the tab IS the way back,
       and a back link that leaves the tab you are standing on is a trap. Those
       two pages are the only .et-page-back in the app, so no modifier class is
       needed — and none is added, which keeps the browser markup byte-identical
       to what it was. `.et-page-back + .et-page-hero` still matches a hidden
       button (display:none does not leave the sibling chain), and the --tab
       rule above outranks its padding reset, so the hero keeps the 16px. */
    html.et-shell-bar .et-page-back { display: none; }

    /* ── 4.4 On a group screen the band IS the header ── */
    /* The two routes that wear EtGroupHeader (MainLayout stamps
       data-route="group" on exactly those) get no app top bar in the phone
       shell. The band already carries the back arrow, the group name, the
       members and the gear; the bar above it repeated none of that and cost
       ~64px of a phone first screen for a brand tile and a bell.

       Gated on .et-shell-bar, not .et-standalone as spec §4.4 wrote it. That is
       a DERIVATION from the 2026-09-22 toggle decision rather than something
       the owner asked for in those words: the whole shell treatment moved onto
       the toggle that day, and a group screen sitting under the bottom bar
       with a second header stacked above its band is exactly what §4.4
       removes — in a browser as much as in the installed app. With the switch
       on `never` the top bar comes back here too, which is the escape hatch.

       display:none rather than dropping the header from the DOM: the layout
       renders one header for every screen, and a C# branch would put "which
       chrome does this page get" in two places. */
    html.et-shell-bar .app-shell[data-route="group"] .app-topbar { display: none; }

    /* ── §10 The same trade on the three tab roots ── */
    /* Kezdőlap, Költségek and Profil wear EtPageBand while the `graphic`
       pilot setting is on, and MainLayout stamps data-route="page-band" on
       exactly those three — and only while it IS on, so `plain` brings the
       top bar back by rendering no attribute at all. Everything §4.4 argues
       for the group band holds here verbatim: the band carries the screen's
       name, the bar above it repeated nothing and cost ~64px of a phone's
       first screen. */
    html.et-shell-bar .app-shell[data-route="page-band"] .app-topbar { display: none; }

    /* With the bar gone the band is the topmost element, so it loses the
       inset look it wears on desktop and runs edge to edge — the same
       full-bleed the group band gets from .et-group-view-header's negative
       margins, except that this one is conditional: on desktop the band sits
       INSIDE the content column under the real top bar, where a full-bleed
       plate would be wider than the page it heads. */
    html.et-shell-bar .app-shell[data-route="page-band"] .et-page-band {
        margin: 0 calc(var(--et-space-6) * -1) 16px;
        border-radius: 0;
    }

    /* The bell the hidden top bar took with it. EtPageBand keeps its trailing
       slot display:none (ds.css) precisely so this is the only state that
       shows one — see the note there. */
    html.et-shell-bar .app-shell[data-route="page-band"] .et-page-band-trailing {
        display: flex;
    }

    /* Installed AND showing the shell — the only state in which the band runs
       under a status bar. Same var()-before-env() chain and the same reason as
       the group band above. */
    html.et-standalone.et-shell-bar .app-shell[data-route="page-band"] .et-page-band {
        padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
    }
}

/* .main-content's side padding is 24px from 768px up and 16px below it, and
   the full-bleed rule above has to cancel whichever one applies. A second,
   narrower block rather than a media-range: it must come AFTER the 959.98px
   one to win at equal specificity, and the file's own "Mobile media block"
   sits ~3000 lines above this. */
@media (max-width: 767.98px) {
    html.et-shell-bar .app-shell[data-route="page-band"] .et-page-band {
        margin-left: calc(var(--et-space-4) * -1);
        margin-right: calc(var(--et-space-4) * -1);
    }
}

/* ──────────── Screen header bands (spec 2026-09-19 §10) ────────────
   Page glue only — the band itself is a DS atom (ds.css .et-page-band). This
   is where it sits: the rhythm .et-page-hero would have had in its place, so
   swapping the pilot does not move the content under it. 16px of top gap
   rather than the hero's 28/18px of padding-top, because a tinted plate reads
   heavier than a line of text and wants a little more air above it than
   inside it. */
.et-page-band { margin: 16px 0 22px; }
@media (max-width: 767.98px) {
    .et-page-band { margin: 12px 0 16px; }
}

/* ──────────── pilot toggle (temporary): et:topbar-fade ────────────
   Owner request 2026-09-25: at the top of a page the phone top bar is not a
   white slab with a line under it but a soft gradient from the system colour
   downwards, with no divider. 2026-09-26: five values (ThemeService) —

     off            — the solid bar and its divider, as before the pilot
     bg             — DEFAULT, and what a missing attribute means: the gradient
                      runs into the PAGE background (--et-color-bg, the tint
                      around the cards), so the bar melts into the page
     on             — the first cut: into the palette tint, EtPageBand's first
                      stop, so the plain header and the illustrated band speak
                      the same tint
     bg-sticky,
     palette-sticky — the same two gradients, kept when the page scrolls

   The non-sticky pair hands over to the solid bar with its line once the page
   scrolls (html.et-scrolled, app-init.js) and comes back at the top.

   The top stop is the bar's own solid surface, and it stays the bar's
   background-color in every state — that is what the top-edge observer
   (app-init.js, #481) reads for the strip above the page, so the strip and the
   bar keep meeting seamlessly and the strip never changes colour on scroll. It
   is also what keeps the sticky pair opaque: the gradient's transparent top
   lies over that solid background, never over the page scrolling under the
   bar. The tint is a pseudo-element over it: a gradient cannot be
   transitioned, an opacity can. z-index -1 inside the bar's own stacking
   context (sticky + z-index 1102) paints it above the bar's background and
   below its content.

   The divider does not go transparent: a transparent border shows the bar's
   own background, and the pseudo-element (positioned against the padding box)
   stops above it — which drew a 1px WHITE line under the gradient, a divider
   after all (measured on the dumps). It takes the gradient's last colour
   instead, so that row just continues the ramp. One custom property carries
   that colour to both.

   Phone widths only — the top bar is the phone's whole header there; the
   desktop bar sits in the content frame and keeps its line. */
@media (max-width: 959.98px) {
    html:not([data-topbar-fade="off"]) .app-topbar {
        --et-topbar-fade-end: var(--et-color-bg);
        border-bottom-color: var(--et-topbar-fade-end);
        transition: border-bottom-color var(--et-motion-fast) var(--et-ease-standard);
    }
    html:is([data-topbar-fade="on"], [data-topbar-fade="palette-sticky"]) .app-topbar {
        --et-topbar-fade-end: color-mix(in srgb, var(--et-color-accent) 12%, var(--et-color-surface));
    }
    html:not([data-topbar-fade="off"]) .app-topbar::before {
        content: "";
        position: absolute;
        inset: 0;
        z-index: -1;
        pointer-events: none;
        background: linear-gradient(to bottom, transparent, var(--et-topbar-fade-end));
        transition: opacity var(--et-motion-enter) var(--et-ease-standard);
    }
    html:is(:not([data-topbar-fade]), [data-topbar-fade="bg"], [data-topbar-fade="on"]).et-scrolled .app-topbar {
        border-bottom-color: var(--et-color-border);
    }
    html:is(:not([data-topbar-fade]), [data-topbar-fade="bg"], [data-topbar-fade="on"]).et-scrolled .app-topbar::before {
        opacity: 0;
    }
    /* During a page transition the bar takes its new state at once: the
       transition's cross-fade of the bar (et-topbar, below) is the animation,
       in step with the page. Its own fade would start after the capture and
       run on after it (etMotion.beginPage, app-init.js). */
    html[data-page-motion] .app-topbar,
    html[data-page-motion] .app-topbar::before {
        transition: none;
    }

    /* The group band in a browser tab and in the home-screen web app (owner,
       2026-09-26: "weben és PWA-ban, ahol nem megoldható, hogy a kamera-sziget
       köré bemenjen a tartalom"). There the band replaces the top bar (§4.4,
       .et-shell-bar) but cannot run under the status bar the way it does in
       the native shell, so a system strip sits right above its hard coloured
       edge. Option A: the band's OWN first 28px start on the surface colour and
       fade into the group colour — no strip added, no height added. The native
       shell keeps the band under its status bar untouched (unless et:group-top
       is `system`, below), and `off` keeps the hard edge, so "Vonallal" stays
       the whole before-picture.

       An ::after over the band's background (the gradient is an inline style,
       so it cannot take a second layer from here): later in tree order than the
       photo and the illustration, so their top edge fades with the band's, and
       under the text, which the inner wrapper lifts to z-index 1 — the
       illustrated and photo forms already do, the plain one needs it here.
       app-init.js's top-edge observer reads this pseudo-element's first stop
       while it is painted, so the strip above takes that colour and meets the
       start of the fade.

       2026-09-28: the fade starts on the band's own start colour (--et-gh-top,
       EtGroupBand.StartColor), no longer on the surface. The premise above was
       half wrong: a browser cannot run the band itself around the Dynamic
       Island, but it can paint the strip there in ONE flat colour, and the
       owner chose the group's over white. Safari 26+ ignores the observer's
       theme-color and root background for that strip — it reads a fixed or
       sticky element at the top edge, else <body>'s background-color, which
       is why the strip showed the palette page background here while the top
       bar (sticky, surface) kept every other screen white. The element it
       reads now is .et-group-header-tint below. The surface stays the fallback
       for a caller that sets no --et-gh-top.

       2026-09-29, pilot toggle et:group-top: `system` puts the surface back as
       the start — the fade's first stop and the strip's colour both — so the
       strip is the white/black every other tab shows and the band fades out
       of it (owner: "a rendszer fehér színéből menjen átmenettel a
       groupheader színébe"). --et-gh-edge carries whichever start applies: the
       band's inline --et-gh-top cannot be overridden from here, a second
       property can.

       2026-10-02: `system` reaches the native shell too (owner: in the app the
       group colour still went round the camera island under "Rendszerszín").
       There the band's own status-bar padding IS the strip, so the ramp below,
       which already starts under --et-gh-inset, is all it takes. The status-bar
       icons need nothing: SystemBars follows the theme (app-init.js), which is
       exactly the surface's light/dark. The default `group` keeps the native
       band untouched, and the Safari tint further down stays browser-only. */
    html.et-shell-bar:is(:not(.et-native-shell), [data-group-top="system"]):not([data-topbar-fade="off"]) .app-shell[data-route="group"] :is(.et-group-view-header, .et-group-settings-header) {
        position: relative;
        --et-gh-edge: var(--et-gh-top, var(--et-color-surface));
    }
    html[data-group-top="system"].et-shell-bar:not([data-topbar-fade="off"]) .app-shell[data-route="group"] :is(.et-group-view-header, .et-group-settings-header) {
        --et-gh-edge: var(--et-color-surface);
    }
    html.et-shell-bar:is(:not(.et-native-shell), [data-group-top="system"]):not([data-topbar-fade="off"]) .app-shell[data-route="group"] :is(.et-group-view-header, .et-group-settings-header) .et-group-header-inner {
        position: relative;
        z-index: 1;
    }
    html.et-shell-bar:is(:not(.et-native-shell), [data-group-top="system"]):not([data-topbar-fade="off"]) .app-shell[data-route="group"] :is(.et-group-view-header, .et-group-settings-header)::after {
        content: "";
        position: absolute;
        inset: 0 0 auto;
        height: 28px;
        pointer-events: none;
        /* A 0%-alpha mix of the surface rather than `transparent`, as the photo
           seam in ds.css does, so the ramp cannot grey out mid-way. */
        background: linear-gradient(to bottom,
                    var(--et-gh-edge),
                    color-mix(in srgb, var(--et-gh-edge) 0%, transparent));
    }
    /* Under `system` the band fades in from the white/black above over its top
       three quarters and is the full group colour only in the bottom quarter
       (owner, 2026-10-01, after a 28px, a 48px, a whole-height and a
       first-third ramp). Smoothstep, so the ramp meets the strip above and the
       colour below without a line. It starts below the status-bar padding the
       home-screen web app and the native shell give the band (--et-gh-inset),
       so the clock sits on plain white/black.

       Stop n sits at n% of the area below the inset: n% + (1 − n) × inset. */
    html[data-group-top="system"].et-shell-bar:not([data-topbar-fade="off"]) .app-shell[data-route="group"] :is(.et-group-view-header, .et-group-settings-header)::after {
        height: 100%;
        background: linear-gradient(to bottom,
                    var(--et-gh-edge) var(--et-gh-inset, 0px),
                    color-mix(in srgb, var(--et-gh-edge) 84%, transparent) calc(18.75% + 0.8125 * var(--et-gh-inset, 0px)),
                    color-mix(in srgb, var(--et-gh-edge) 50%, transparent) calc(37.5% + 0.625 * var(--et-gh-inset, 0px)),
                    color-mix(in srgb, var(--et-gh-edge) 16%, transparent) calc(56.25% + 0.4375 * var(--et-gh-inset, 0px)),
                    color-mix(in srgb, var(--et-gh-edge) 0%, transparent) calc(75% + 0.25 * var(--et-gh-inset, 0px)));
    }
    /* In dark mode the title sits on the dark half of that ramp, where the
       band's fixed dark ink (ds.css: "on the light band") disappears, so it
       turns white. The member count below it is already on the near-full band
       and keeps its dark ink. */
    html[data-group-top="system"][data-theme="dark"].et-shell-bar:not([data-topbar-fade="off"]) .app-shell[data-route="group"] :is(.et-group-view-header, .et-group-settings-header) .et-group-header-title {
        color: var(--et-cta-ink);
    }

    /* Safari's source for the strip. What WebKit does (read from its source,
       LocalFrameView::fixedContainerEdges and Page::updateFixedContainerEdges,
       2026-09-29 — the 4px strip #505/#509 shipped was never read, owner
       device report the same day):
         - it hit-tests ONE point, the middle of the viewport 4px below its
           top edge, and walks up from the element it hits to the first
           fixed or sticky ancestor, stuck or not;
         - that element gives its colour only if it is more than 10px in both
           directions, at least 90% of the viewport wide, visible, and has a
           plain background-color (the style value, not the painted pixels);
           else Safari samples the pixels there — most likely how the old
           strip turned up now and then after a pull-to-refresh, when the
           pull had moved it under that point;
         - when a later look finds nothing, it keeps the last element's colour
           for as long as that element is visibility: visible.
       So: 16px tall and full width, over the band's top edge (the negative
       margin gives the height back), z-index 2 over the inner wrapper and the
       photo so the hit lands on it. It paints nothing — all of it is padding,
       and background-clip: content-box leaves the colour a 0px box to fill —
       so it can never show as a bar over the band when the page scrolls
       (owner, 2026-09-29: "látom a 4 px-es sávot ami mozog"). Its colour is
       the fade's start (--et-gh-edge above), so the strip and the band meet.
       Sticky inside the band, not fixed (owner 2026-09-28: past the band, the
       palette background): it leaves with the band's bottom edge, and once
       it is off screen app-init.js marks it --gone, visibility: hidden, so
       Safari lets go of it and falls back to <body> — the page background the
       content is then scrolling on. Sticky needs the band to be no scroll
       container, hence overflow: clip on its photo and illustrated forms
       (ds.css). pointer-events: none keeps the corner buttons' top 16px
       tappable; Safari's first look ignores pointer-events. iOS WebKit only
       (-webkit-touch-callout exists nowhere else): Chrome on Android takes
       theme-color from the observer instead, and a desktop browser narrowed
       below 960px has no strip to feed. */
    @supports (-webkit-touch-callout: none) {
        html.et-shell-bar:not(.et-native-shell):not([data-topbar-fade="off"]) .app-shell[data-route="group"] .et-group-header-tint {
            display: block;
            position: sticky;
            top: 0;
            height: 0;
            padding-top: 16px;
            margin-bottom: -16px;
            background-color: var(--et-gh-edge);
            background-clip: content-box;
            pointer-events: none;
            z-index: 2;
        }
        html.et-shell-bar:not(.et-native-shell):not([data-topbar-fade="off"]) .app-shell[data-route="group"] .et-group-header-tint--gone {
            visibility: hidden;
        }
    }
}

/* ──────────── pilot toggle (temporary): et:group-bar ────────────
   Owner request 2026-09-29: once the group band has scrolled away, nothing on
   a group screen holds the top of the page ("most jelenleg ha legörgetünk
   akkor nincsen rögzített kontent az oldal tetején"). `on` brings back the
   ordinary top bar of the other tabs there — the brand tile, the bell, and
   the group's icon and name as its title (MainLayout.TopBarTitle) — sliding
   down from above the screen as the band's bottom edge goes under it, and
   back up when the band returns. `off` (the default) keeps §4.4 as it is:
   no top bar on a group screen at all.

   Fixed rather than sticky: the bar would otherwise take its 64px back in
   the flow above the band. Parked above the screen and visibility: hidden
   while the band is in view, so it neither shows, takes taps, nor counts
   for Safari's strip; shown, it is the fixed element Safari reads, and the
   strip turns the bar's white, as on the other tabs. html.et-group-bar-shown
   comes from app-init.js (the band's IntersectionObserver). visibility waits
   out the exit slide; it flips at once on the way in.

   No view-transition-name while it is parked: a page transition would
   capture it above the screen and fly it down into the next page's bar.
   Out of the way, the next page's bar simply fades in, as it always has
   coming off a group screen. The transform is `none` once shown — a
   transformed bar would be the containing block of the bell's fixed phone
   sheet.

   The slide belongs to the band scrolling, not to arriving on the screen.
   Coming from a tab, the bar enters a group route unparked, and the exit
   slide above ran on it: for ~200ms the bar, titled with the group, stood
   over the band and slid off past the camera island although the page sat
   at the top (owner, 2026-09-30). During a page transition it parks at
   once, as the plain bar takes its new state at once there (the
   data-page-motion rule of the et:topbar-fade block). Without View
   Transitions (iOS < 18) nothing marks that moment, and the slide still
   plays once there. */
/* In shell.css although data-route="group" names a Kassza screen: it is top-bar
   mechanism, and it has to stay after the et:topbar-fade block above. */
@media (max-width: 959.98px) {
    html[data-group-bar="on"].et-shell-bar .app-shell[data-route="group"] .app-topbar {
        display: flex;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        transform: none;
        transition: transform var(--et-motion-enter) var(--et-ease-enter), visibility 0s;
    }
    html[data-group-bar="on"]:not(.et-group-bar-shown).et-shell-bar .app-shell[data-route="group"] .app-topbar {
        transform: translateY(-100%);
        visibility: hidden;
        view-transition-name: none;
        transition: transform var(--et-motion-exit) var(--et-ease-exit), visibility 0s linear var(--et-motion-exit);
    }
    html[data-group-bar="on"][data-motion="off"].et-shell-bar .app-shell[data-route="group"] .app-topbar,
    html[data-group-bar="on"][data-page-motion].et-shell-bar .app-shell[data-route="group"] .app-topbar {
        transition: none;
    }
}

/* ──────────── pilot toggle (temporary): et:motion — page transitions ────────────
   Owner request 2026-09-25: smoother tab switches, and deeper screens (group
   settings) coming in from the side. etMotion.beginPage (app-init.js) wraps a
   route change in a View Transition and stamps data-page-motion for its
   duration; State/PageMotion.cs picks the kind:

     push — deeper: the old page drifts left and fades, the new one comes in
            from the right. Material's shared X axis — a 48px slide with a fade,
            not a full-width swipe: a snapshot is only as opaque as the page
            under it, and a short slide never shows the gap a long one would.
     pop  — the same axis in reverse.
     tab  — a sibling, not a place further in: fade-through, no slide.

   The ROOT is what moves — the whole viewport as the user sees it, scroll
   position included (naming .main-content instead would fly a scrolled page
   in from its offset). The top bar and the bottom bar get names of their own
   so they hold still above and below it and only cross-fade what changed: the
   tab title, the active tab. Names only while a transition runs, so nothing
   here can touch the page at rest.

   Browsers without View Transitions (iOS < 18) simply swap the page, as
   before; so do reduced motion and data-motion="off" — etMotion never starts a
   transition there, which is the only gate that reaches these pseudo-elements
   (tokens.css's blanket reduced-motion rule does not). The same gate keeps
   the page transitions off from 960px up (owner 2026-09-28: fine on the
   phone, odd on a desktop, where the whole wide screen moves). */
html[data-page-motion] .app-topbar { view-transition-name: et-topbar; }
html[data-page-motion] .et-bottom-bar { view-transition-name: et-bottombar; }

::view-transition-group(et-topbar),
::view-transition-group(et-bottombar) {
    animation-duration: var(--et-motion-enter);
    animation-timing-function: var(--et-ease-standard);
}
::view-transition-old(et-topbar),
::view-transition-new(et-topbar),
::view-transition-old(et-bottombar),
::view-transition-new(et-bottombar) {
    animation-duration: var(--et-motion-exit);
}

@keyframes et-page-out-left  { to   { opacity: 0; transform: translateX(-48px); } }
@keyframes et-page-in-right  { from { opacity: 0; transform: translateX(48px); } }
@keyframes et-page-out-right { to   { opacity: 0; transform: translateX(48px); } }
@keyframes et-page-in-left   { from { opacity: 0; transform: translateX(-48px); } }
@keyframes et-page-fade-out  { to   { opacity: 0; } }
@keyframes et-page-fade-in   { from { opacity: 0; transform: scale(0.98); } }

/* The old page is out of the way before the new one has fully arrived: exit on
   the exit pair, enter on the enter pair, 60ms behind. */
html[data-page-motion="push"]::view-transition-old(root) {
    animation: et-page-out-left var(--et-motion-exit) var(--et-ease-exit) both;
}
html[data-page-motion="push"]::view-transition-new(root) {
    animation: et-page-in-right var(--et-motion-enter) var(--et-ease-enter) 60ms both;
}
html[data-page-motion="pop"]::view-transition-old(root) {
    animation: et-page-out-right var(--et-motion-exit) var(--et-ease-exit) both;
}
html[data-page-motion="pop"]::view-transition-new(root) {
    animation: et-page-in-left var(--et-motion-enter) var(--et-ease-enter) 60ms both;
}
/* Fade-through: the old page goes quickly, the new one settles in from 98%. */
html[data-page-motion="tab"]::view-transition-old(root) {
    animation: et-page-fade-out 90ms linear both;
}
html[data-page-motion="tab"]::view-transition-new(root) {
    animation: et-page-fade-in 210ms var(--et-ease-enter) 90ms both;
}

/* ──────────── pilot toggle (temporary): et:motion — inside the sheet ────────────
   Owner decisions 2026-09-26, all through etMotion (app-init.js) and so behind
   the same gates as the page transitions: data-motion="off", reduced motion,
   and no View Transitions at all on iOS < 18, where each of these simply
   swaps.

   Panel → panel (data-panel-motion = push | pop). Tartozás rendezése → a split
   row → the expense used to replay the sheet's rise; now the sheet holds still
   (EtPanel --swapped, ds.css) and only its content moves, on the page
   transition's own axis and timing: out 48px to the left and faded on the exit
   pair, in from the right on the enter pair 60ms behind; pop the mirror image.
   The root is frozen — the page behind, the dim and the shell itself do not
   change — and the header, body and footer (et-panel-head/-body/-foot) are
   clipped to their own boxes so the drift never draws outside the panel. */
html[data-panel-motion="push"]::view-transition-old(root),
html[data-panel-motion="push"]::view-transition-new(root),
html[data-panel-motion="pop"]::view-transition-old(root),
html[data-panel-motion="pop"]::view-transition-new(root) {
    animation: none;
}
::view-transition-group(et-panel-head),
::view-transition-group(et-panel-body),
::view-transition-group(et-panel-foot) {
    animation-duration: var(--et-motion-enter);
    animation-timing-function: var(--et-ease-standard);
    overflow: clip;
}
html[data-panel-motion="push"]::view-transition-old(et-panel-head),
html[data-panel-motion="push"]::view-transition-old(et-panel-body),
html[data-panel-motion="push"]::view-transition-old(et-panel-foot) {
    animation: et-page-out-left var(--et-motion-exit) var(--et-ease-exit) both;
}
html[data-panel-motion="push"]::view-transition-new(et-panel-head),
html[data-panel-motion="push"]::view-transition-new(et-panel-body),
html[data-panel-motion="push"]::view-transition-new(et-panel-foot) {
    animation: et-page-in-right var(--et-motion-enter) var(--et-ease-enter) 60ms both;
}
html[data-panel-motion="pop"]::view-transition-old(et-panel-head),
html[data-panel-motion="pop"]::view-transition-old(et-panel-body),
html[data-panel-motion="pop"]::view-transition-old(et-panel-foot) {
    animation: et-page-out-right var(--et-motion-exit) var(--et-ease-exit) both;
}
html[data-panel-motion="pop"]::view-transition-new(et-panel-head),
html[data-panel-motion="pop"]::view-transition-new(et-panel-body),
html[data-panel-motion="pop"]::view-transition-new(et-panel-foot) {
    animation: et-page-in-left var(--et-motion-enter) var(--et-ease-enter) 60ms both;
}

/* Event card ⇄ event summary (data-panel-motion = hero-open | hero-close). The
   card's media and the summary's hero share et-event-hero, so the picture
   flies between the two while the sheet (et-panel-sheet) rises under it — or
   drops away as it flies back. The root cross-fades, which is the dim coming
   and going. The picture is cropped to the flying box (cover, clipped), since
   a square card image and the wide hero never share an aspect ratio; and it is
   raised above the sheet, which on the way in is named after it and would
   otherwise paint over it. The 640+ side panel has no rise of its own, so there
   it only fades. */
html[data-panel-motion="hero-open"]::view-transition-old(root),
html[data-panel-motion="hero-open"]::view-transition-new(root) {
    animation-duration: var(--et-motion-enter);
}
html[data-panel-motion="hero-close"]::view-transition-old(root),
html[data-panel-motion="hero-close"]::view-transition-new(root) {
    animation-duration: var(--et-motion-exit);
}
::view-transition-group(et-event-hero) {
    z-index: 1;
    overflow: clip;
    border-radius: var(--et-card-radius-lg);
    animation-duration: var(--et-motion-enter);
    animation-timing-function: var(--et-ease-standard);
}
::view-transition-old(et-event-hero),
::view-transition-new(et-event-hero) {
    height: 100%;
    object-fit: cover;
    animation-duration: var(--et-motion-enter);
}
::view-transition-group(et-panel-sheet) {
    animation-duration: var(--et-motion-enter);
}
@media (max-width: 639.98px) {
    html[data-panel-motion="hero-open"]::view-transition-new(et-panel-sheet) {
        animation: et-sheet-rise var(--et-motion-enter) var(--et-ease-enter) both;
    }
    html[data-panel-motion="hero-close"]::view-transition-old(et-panel-sheet) {
        animation: et-sheet-drop var(--et-motion-exit) var(--et-ease-exit) both;
    }
}

/* A todo leaving its shelf (data-list-motion, NeedsAttentionCarousel). Each
   card fully on screen carries a name of its own (class et-list-item): a card
   in both states slides to its new place — the neighbours closing the gap on
   the desktop grid — the card that left fades and shrinks, and a card that has
   come into view (the next page of the phone's one-per-page strip) slides in
   from the right. The rest of the page does not animate. */
html[data-list-motion]::view-transition-old(root),
html[data-list-motion]::view-transition-new(root) {
    animation: none;
}
@keyframes et-list-out { to { opacity: 0; transform: scale(0.92); } }
::view-transition-group(*.et-list-item) {
    animation-duration: var(--et-motion-enter);
    animation-timing-function: var(--et-ease-standard);
}
::view-transition-old(*.et-list-item):only-child {
    animation: et-list-out var(--et-motion-exit) var(--et-ease-exit) both;
}
::view-transition-new(*.et-list-item):only-child {
    animation: et-page-in-right var(--et-motion-enter) var(--et-ease-enter) 60ms both;
}

/* ── et:motion — GroupSettings tab content (owner, 2026-09-26) ──
   The tab row's thumb slides (ds.css, EtSegmentedControl) and the content
   under it follows: the new tab comes in from the side its tab sits on, on
   the same 48px shared X axis + fade as a pushed or popped page — the very
   keyframes above. Only the arrival moves. An exit would need the old tab
   kept mounted next to the new one, two tabs' worth of components at once,
   for 200ms of motion the arrival already carries.

   `backwards`, not `both`: once the entry is over nothing of it stays on the
   element. A filled transform would keep the panel the containing block of
   the fixed-position things inside it (EtDropdown's phone sheet).

   Gated like the rest of this section: data-motion="off" here, reduced motion
   by tokens.css's blanket animation-duration rule, and phone widths only, like
   the page transitions (etMotion's desktop gate) — from 960px up the new tab
   simply replaces the old one. GroupSettings sets the direction class only on
   a switch, never on the first render. */
@media (max-width: 959.98px) {
    html:not([data-motion="off"]) .et-group-settings-panel--from-right {
        animation: et-page-in-right var(--et-motion-enter) var(--et-ease-enter) backwards;
    }
    html:not([data-motion="off"]) .et-group-settings-panel--from-left {
        animation: et-page-in-left var(--et-motion-enter) var(--et-ease-enter) backwards;
    }
}

/* The stage clips that 48px. A full-width panel shifted right sticks out past
   the page for 300ms — long enough to flash a horizontal scrollbar on a
   desktop, and to let a swipe pan the page sideways on a phone. overflow-x:
   clip, not hidden: hidden would turn the stage into a scroll container and
   clip vertically too (the member row menus open downwards). The clip edge is
   the page's own edge, not the panel's: the stage cancels .main-content's side
   padding and restores it inside — the full-bleed pattern the group band above
   uses, 24px from 768px up and 16px below — so the cards' shadows keep the
   padding to fall into. At rest it changes nothing: a margin and a padding
   that cancel, and a clip at a line nothing crosses. */
.et-group-settings-stage {
    margin-left: calc(var(--et-space-6) * -1);
    margin-right: calc(var(--et-space-6) * -1);
    padding: 0 var(--et-space-6);
    overflow-x: clip;
}
@media (max-width: 767.98px) {
    .et-group-settings-stage {
        margin-left: calc(var(--et-space-4) * -1);
        margin-right: calc(var(--et-space-4) * -1);
        padding: 0 var(--et-space-4);
    }
}

/* Onboarding part 5 (spec 2026-10-02-onboarding-pro-tips): the behaviour tips reuse
   .et-tip (OnboardingTip) and the existing EtSurface/row vocabulary as-is — no new rules
   needed for those. The two blocks below are new: the first-steps cards' one-time
   "finished" line (shared by FirstStepsCard and JoinerStepsCard — only one of the two is
   ever mounted-and-finished for a given viewer) and the celebration burst etDs.celebrate
   spawns from either that line or the toast host. */
.et-first-steps-done-line {
    padding: var(--et-space-4, 16px) 0;
    text-align: center;
    font-weight: 600;
    color: var(--et-color-accent);
}

/* Fixed-position pieces, animated transform/opacity only via the Web Animations API
   (ds.js) — never spawned at all when et:motion is off or prefers-reduced-motion is on,
   so this media query is a belt-and-braces backstop, not the actual gate. */
.et-celebrate-piece {
    position: fixed;
    width: 6px;
    height: 6px;
    border-radius: 2px;
    pointer-events: none;
    z-index: 9999;
}
@media (prefers-reduced-motion: reduce) {
    .et-celebrate-piece { display: none; }
}
