/* ====== PLAIN BUTTON RESET ======
   Strips a native <button>'s default chrome (grey fill, border, padding,
   centred text, system font) so a real button can stand in for a clickable
   <div>/<span> without changing how it looks — but now keyboard-reachable and
   activatable with Enter/Space. Apply ALONGSIDE the element's existing class,
   e.g. class="btn-plain gender-opt": the component's own (scoped) styles win
   over this global utility, so they keep dictating the look; btn-plain only
   neutralises the browser defaults the <div> never had.

   Note: a <div> is block (full width) but a <button> is inline-block (shrinks
   to content). For full-width rows, the element's own class must set width/
   display (we don't force it here, so inline buttons aren't stretched). */
.btn-plain {
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: none;
    margin: 0;
    padding: 0;
    font: inherit;
    color: inherit;
    text-align: inherit;
    cursor: pointer;
}
/* btn-plain strips all chrome, so keyboard focus would be invisible. Give it a
   blue INSET ring (the same cue as the keyboard-active dropdown option, .lb-active)
   — inset so it follows the element's own radius and never clips inside a
   scrolling/overflow parent. Keyboard-only; scoped look still comes from the
   element's own class. */
.btn-plain:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--blue-primary);
}

/* ====== APP-WIDE KEYBOARD FOCUS FALLBACK ======
   tokens.css resets the browser focus ring app-wide, so any button/link WITHOUT
   its own focus cue (the many custom-classed buttons: .cat-pill, .filter-btn,
   chips, etc.) would be invisible to keyboard users. This is the safety net: a
   blue ring (our interaction color) shown ONLY for keyboard nav (:focus-visible
   never fires on mouse/touch — those still get :hover/:active).

   Specificity is kept low (element selector + pseudo = 0,1,1) so a bespoke
   `.class:focus-visible` (0,2,0) that sets `outline` (e.g. .btn-*, .btn-plain,
   pickers, cards) overrides it. BUT most bespoke cues only recolor `border-color`
   — a different property — so they DON'T override this outline and would show a
   doubled ring. Those controls (chips/pills/tabs that already light their border)
   are opted out explicitly below. Inputs are excluded — they light up via their
   wrapper's :focus-within. */
button:focus-visible,
a:focus-visible,
summary:focus-visible {
    outline: 2px solid var(--blue-primary);
    outline-offset: 2px;
}

/* Opt-out: these controls already cue keyboard focus by lighting their OWN
   border (a bespoke `.class:focus-visible { border-color }`). Suppress the global
   outline on them so they show a single light-up border, not a double ring.
   (0,2,0 specificity beats the 0,1,1 fallback above.) */
.cat-pill:focus-visible,
.cat-tab:focus-visible,
.cat-chip:focus-visible,
.filter-chip:focus-visible,
.wallet-filter-chip:focus-visible,
.fp-pill:focus-visible,
.topic-chip:focus-visible,
.skill-chip:focus-visible,
.quick-btn:focus-visible,
.search-tab:focus-visible,
.jb-date-chip:focus-visible,
.jb-slot-chip:focus-visible,
.bsp-date-chip:focus-visible,
.bsp-slot-chip:focus-visible {
    outline: none;
}

/* Header icon buttons on the blue gradient header already cue focus with a
   translucent-white background-bump — drop the blue outline ring (Susanti: the
   ring is redundant + ugly there; the existing bump is enough). */
.icon-btn:focus-visible,
.app-header-back:focus-visible,
.header-settings:focus-visible,
.filter-btn:focus-visible,
.search-gps:focus-visible,
.login-close:focus-visible,
.kj-mitra-header:focus-visible,
.nl-mark-all:focus-visible {
    outline: none;
}

/* ====== TEXT LINKS (plain blue-text buttons that act as hyperlinks) ======
   Shared cue for any borderless blue-text "link" button (.section-link,
   .search-see-all, .btn-resend, .link-btn, etc.). Add `text-link` ALONGSIDE the
   element's own class and wrap its label text in <span class="text-link-label">.
   The element's own (scoped) class still owns the base look (color/size/width);
   this only standardises the interaction cue:
     • hover  → underline the label (conventional link affordance)
     • keyboard focus → soft blue-pale PILL hugging the label, NOT the
       full-width fallback ring (the ring around a 100%-wide link reads as an
       oversized box). `.text-link:focus-visible` (0,2,0) beats the global
       `button:focus-visible` fallback (0,1,1) above, so it opts out of the ring.
   The pill lives on the inner label so it hugs the words even when the button
   is full-width. The negative inline margin cancels the pill's horizontal
   padding so adding the span doesn't shift the link's position in a row. */
.text-link-label {
    display: inline-block;
    padding: 3px 12px;
    margin: 0 -12px;
    border-radius: 999px;
    transition: background 0.15s;
}
.text-link:focus-visible { outline: none; }
.text-link:focus-visible .text-link-label { background: var(--blue-pale); }
/* Hover mirrors the keyboard-focus cue: the same blue-pale pill, no underline.
   Guarded so a touch tap on an in-place link (e.g. Batal, resend) doesn't leave
   the pill stuck. */
@media (hover: hover) {
    .text-link:hover .text-link-label {
        background: var(--blue-pale);
    }
}

/* ====== MINIMUM TAP TARGET ======
   Grows a control's HIT AREA to 44 x 44 without changing a single visible pixel.

   Why 44: a fingertip's contact patch is roughly 8-10mm, but the phone reports only its
   centre point, which the user cannot see. Below ~44 the miss rate climbs, and a miss
   lands on whatever sits next to the control. It is Apple's figure and WCAG's AAA one
   (2.5.5); Android asks 48. Measured at 390px with the app's 1.1 display zoom, several
   controls sat well under: the banner dismiss x at 31, the Search GPS button at 33,
   "Lihat Semua" at 25 tall, the banner's own "Aktifkan" button at 29.

   How it works: a transparent ::after, centred on the control, sized to whichever is
   larger of the control's own box and 44px per axis. So a control that is already wide
   enough only grows vertically, and one that is short in both grows in both. A
   pseudo-element is part of its originating element for hit-testing, so a press on it
   activates the button.

   Deliberately NOT min-height on the element itself: that changes layout and, on a pill
   or a chip, changes how the control looks. The whole point is that the design stays as
   drawn and only the reach grows.

   Two things to check when adding this to a control:
     1. The control must not be inside an ancestor that CLIPS it. An overflow:hidden or
        overflow-x:auto parent (a chip strip) crops the overhang, so the extra area has to
        fit inside that parent's own padding. Verify by pressing the corner, not by reading
        the CSS.
     2. Watch for a neighbour underneath. The area is invisible, so an overlap silently
        steals the neighbour's taps. Space permitting, prefer growing into padding.

   `position: relative` is set here so callers cannot forget the positioning context the
   ::after needs. CSS cannot say "only if currently static", so read this carefully: a
   component that positions its own control keeps that position, because Blazor scoped CSS
   carries an extra [b-…] attribute and therefore outranks this file — that is what keeps
   `.banner-close`'s `position: absolute` intact. A control positioned from a GLOBAL
   stylesheet has no such protection, so check it still sits where it should after adding
   the class. */
.tap-44 { position: relative; }
.tap-44::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: max(100%, 44px);
    height: max(100%, 44px);
    /* No paint of any kind — this exists purely to be pressed. */
}

/* ====== ZOOMABLE IMAGE THUMBNAIL CUE ======
   Every lightbox-opening image button carries `.photo-btn`. On hover/keyboard
   focus a dark scrim + centered magnifier fades in to signal "tap to enlarge".
   Centered, so it's NEVER clipped by a scroll strip (an outset ring was — that's
   the bug this replaces), and it works on any image size/shape, light & dark.
   Hero / catalog / header-gallery / profile images are NOT .photo-btn → excluded.
   Radius 8px matches most thumbnails; a 10–12px image shows only a negligible
   corner sliver under the 42%-opacity scrim. */
/* Fresh-sheet hover suppression — global so it crosses Blazor scope boundaries
   (these list items live in Settings/PhotoSourceSheet, not AppSheet). slide-sheet.js
   adds .aps-fresh on the sheet on open and removes it on the first pointer move;
   while present, an item sitting under the stationary cursor (the sheet slid up
   beneath it) must NOT show its :hover highlight. !important to win over the
   components' own scoped :hover rules.
   NOTE: the .dpf-item variant (DatePicker/TimePicker wheels) now lives in those
   components' own scoped CSS so it travels with them — the bare `.aps-fresh`
   ancestor there stays unscoped and matches this same sheet element. */
.aps-fresh .theme-option:hover:not(.active),
.aps-fresh .lang-option:hover:not(.active),
.aps-fresh .pss-option:hover { background: transparent !important; }

.photo-btn { position: relative; cursor: zoom-in; }
/* Scrim layer (::before) — dark wash, fades in. Radius matches the thumbnail
   (8px default; override ::before per-class for larger corners). */
.photo-btn::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 8px;
    background: rgba(13,33,55,0.42);
    opacity: 0;
    transition: opacity 0.18s;
    pointer-events: none;
}
/* Magnifier layer (::after) — POPS in (fade + scale-up) for a touch of life, so
   the cue is intuitive (signals "enlarge") AND feels alive. Sits above the scrim. */
.photo-btn::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 26px;
    height: 26px;
    transform: translate(-50%, -50%) scale(0.6);
    background: center / 26px 26px no-repeat;
    /* The other icon that cannot be an <Icon>: a background-image is its own
       document and reaches neither the component nor the --icon-* tokens, so the
       silhouette is drawn three times here to carry its own sheen and shade. The
       shape matches ic-kaca-pembesar in IconDefs.razor and has to be kept in step
       by hand. Written unencoded for legibility; a data URI in a double-quoted
       url() only needs the # escaped. */
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><defs><linearGradient id='s' x1='0' y1='0' x2='0' y2='1'><stop offset='0' stop-color='%23ffffff' stop-opacity='.40'/><stop offset='.34' stop-color='%23ffffff' stop-opacity='.104'/><stop offset='1' stop-color='%23ffffff' stop-opacity='0'/></linearGradient><linearGradient id='d' x1='0' y1='0' x2='0' y2='1'><stop offset='0' stop-color='%23061A2E' stop-opacity='0'/><stop offset='.64' stop-color='%23061A2E' stop-opacity='.040'/><stop offset='1' stop-color='%23061A2E' stop-opacity='.220'/></linearGradient></defs><g stroke='none'><path fill='%23ffffff' d='M2.4 11A8.6 8.6 0 0 1 19.6 11A8.6 8.6 0 0 1 2.4 11ZM5.5 11A5.5 5.5 0 0 0 16.5 11A5.5 5.5 0 0 0 5.5 11ZM18.66 16.54L21.66 19.54A1.5 1.5 0 0 1 19.54 21.66L16.54 18.66A1.5 1.5 0 0 1 18.66 16.54Z'/><path fill='url(%23s)' d='M2.4 11A8.6 8.6 0 0 1 19.6 11A8.6 8.6 0 0 1 2.4 11ZM5.5 11A5.5 5.5 0 0 0 16.5 11A5.5 5.5 0 0 0 5.5 11ZM18.66 16.54L21.66 19.54A1.5 1.5 0 0 1 19.54 21.66L16.54 18.66A1.5 1.5 0 0 1 18.66 16.54Z'/><path fill='url(%23d)' d='M2.4 11A8.6 8.6 0 0 1 19.6 11A8.6 8.6 0 0 1 2.4 11ZM5.5 11A5.5 5.5 0 0 0 16.5 11A5.5 5.5 0 0 0 5.5 11ZM18.66 16.54L21.66 19.54A1.5 1.5 0 0 1 19.54 21.66L16.54 18.66A1.5 1.5 0 0 1 18.66 16.54Z'/></g></svg>");
    opacity: 0;
    transition: opacity 0.18s, transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
    pointer-events: none;
}
/* Suppress the global outline AND the btn-plain inset ring — the scrim + magnifier
   is the only focus cue (no blue border). */
.photo-btn:focus-visible { outline: none; box-shadow: none; }
.photo-btn:focus-visible::before,
.photo-btn:focus-visible::after { opacity: 1; }
.photo-btn:focus-visible::after { transform: translate(-50%, -50%) scale(1); }
@media (hover: hover) {
    .photo-btn:hover::before { opacity: 1; }
    .photo-btn:hover::after { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}

/* ====== PAGE HEADER MIN-HEIGHT (consistency across pages) ====== */
.header,
.hdr,
.legal-header,
.vktp-header,
.keamanan-header,
.settings-header,
.profile-header {
    min-height: 96px;
    box-sizing: border-box;
}

/* ====== AUTO-LOADING BUTTON SPINNER ======
   Driven by js/loading-button.js — adds .is-loading to any <button> that
   becomes disabled within ~600ms of being clicked. Hitchhikes on the existing
   SubmitGuard / disabled-attribute pattern across the app, so no per-page
   edits are required to get loading feedback.

   Layout: spinner is an absolutely-positioned ::after on top of the button;
   the original label is hidden (color: transparent) but kept in the layout
   so the button's size doesn't jump on loading start/end. currentColor on
   the spinner inherits the button's text color, so primary (white spinner
   on blue), secondary (blue on white), social, and ghost variants all
   render correctly without per-variant overrides.

   Opt out by adding `data-no-loading` to a button — the JS skips it.

   The .btn-spinner class is also exposed for the <LoadingButton> Razor
   component, which is kept around for the rare cases that need an explicit
   loading label (e.g., "Mengirim...") instead of just a spinner. */
button.is-loading {
    position: relative;
    pointer-events: none;
    /* Transparentize the text content so plain text nodes (which can't be
       targeted via > *) disappear. The button's actual color was captured by
       the JS into --btn-spin-color BEFORE this rule applied, so the spinner
       below stays visible. */
    color: transparent !important;
    text-shadow: none !important;
}
/* Hide element children (icons, spans) without removing them from layout, so
   the button's intrinsic width/height doesn't jump on loading start/end. */
button.is-loading > * {
    visibility: hidden;
}
button.is-loading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border: 2px solid transparent;
    border-top-color: var(--btn-spin-color, #fff);
    border-right-color: var(--btn-spin-color, #fff);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    opacity: 0.85;
}

.btn-spinner {
    width: 16px;
    height: 16px;
    border: 2px solid transparent;
    border-top-color: currentColor;
    border-right-color: currentColor;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    display: inline-block;
    vertical-align: middle;
    opacity: 0.85;
}
.btn-loading-label {
    margin-left: 8px;
    vertical-align: middle;
}

/* ====== FILTER LOADING (inline spinner when filter changes) ====== */
.filter-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 48px 0;
}
.filter-loading .loading-spinner {
    width: 28px;
    height: 28px;
    border: 3px solid var(--border);
    border-top-color: var(--blue-primary);
    border-radius: 50%;
    animation: spin .6s linear infinite;
}

/* ====== ADDRESS SUGGESTIONS (shared) ====== */
.addr-suggestions {
    padding: 6px;
    background: var(--sug-bg);
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
    border-radius: 14px;
    border: 1px solid var(--sug-border);
    position: relative;
    z-index: 10;
}

.addr-sug-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 10px;
    font-weight: 600;
    color: var(--sug-label);
    padding: 4px 8px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.addr-sug-label svg { color: var(--sug-label); }

.addr-sug-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    padding: 9px 10px;
    cursor: pointer;
    border-radius: 10px;
    font-family: inherit;
    transition: all 0.2s ease;
}

.addr-sug-item:hover,
.addr-sug-item:focus-visible { background: var(--sug-hover); outline: none; }
.addr-sug-item:active { background: var(--sug-active); transform: scale(0.98); }

.addr-sug-item svg {
    width: 16px;
    height: 16px;
    color: var(--sug-icon);
    flex-shrink: 0;
}

.addr-sug-item span {
    font-size: 12.5px;
    font-weight: 500;
    line-height: 1.4;
    color: var(--sug-text);
}

/* ====== LAZY LOAD SPINNER (reusable) ====== */
.lazy-load-more {
    display: flex;
    justify-content: center;
    padding: 16px 0;
}

.lazy-load-spinner {
    width: 24px;
    height: 24px;
    border: 3px solid var(--border);
    border-top-color: var(--blue-primary);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
}

/* ====== CARD CLICKABLE (reusable lift hover) ====== */
/* Usage: add `card-clickable` class to any clickable card that navigates elsewhere.
   Uses inset ring via box-shadow instead of real border so it works on ANY card
   (no layout shift, no conflict with existing borders). */
.card-clickable {
    cursor: pointer;
    transition: transform .2s, box-shadow .2s;
}
.card-clickable:hover,
.card-clickable:focus-visible {
    outline: none;
    /* Themed: a theme that raises the card's resting elevation has to raise its
       hover too, or the lift stops reading. The blue-pale ring needs no token —
       a 0-blur spread follows the element's own border-radius, so it rounds with
       whatever --r-card the theme sets. */
    transform: var(--card-hover-transform);
    box-shadow: var(--card-hover-shadow);
}

/* ====== FORM CARD FLOW: two columns that pack independently ======
   A form is filled in order, so on a wide screen the second column should take the
   even-numbered cards: 1,3,5 down the left and 2,4,6 down the right.

   Neither of the obvious mechanisms does that on its own. Multi-column packs each
   column tightly but fills the whole left column first (1,2,3 then 4,5,6), and where
   it breaks depends on content height. A two-column grid assigns the cards correctly
   but makes every row as tall as its tallest card, which either leaves a hole under
   the shorter one (measured: 171px on Posting at 1440) or stretches it to fill.

   So the columns are real elements. Each `.cf-col` is its own flex column, which
   packs tight with no shared row heights and nothing stretched. The page's markup
   puts the odd cards in the first and the even cards in the second.

   Below the two-column width the wrappers are `display: contents`, so they leave the
   box tree entirely and their cards become children of this flex column — where the
   `order` rules below interleave them back into the order a person fills them in.
   That is why this is flex rather than block at every width: `order` needs a flex
   container, and it is what lets the markup be split by column without the phone
   inheriting a scrambled order. */
.tablet-card-flow.card-flow-form {
    display: flex;
    flex-direction: column;
}
.card-flow-form > .cf-col { display: contents; }

/* Interleave: first column's nth card is 2n-1, second column's is 2n. Written out to
   six per column, which is more cards than any form here has. */
.card-flow-form > .cf-col:nth-child(1) > :nth-child(1)  { order: 1; }
.card-flow-form > .cf-col:nth-child(2) > :nth-child(1)  { order: 2; }
.card-flow-form > .cf-col:nth-child(1) > :nth-child(2)  { order: 3; }
.card-flow-form > .cf-col:nth-child(2) > :nth-child(2)  { order: 4; }
.card-flow-form > .cf-col:nth-child(1) > :nth-child(3)  { order: 5; }
.card-flow-form > .cf-col:nth-child(2) > :nth-child(3)  { order: 6; }
.card-flow-form > .cf-col:nth-child(1) > :nth-child(4)  { order: 7; }
.card-flow-form > .cf-col:nth-child(2) > :nth-child(4)  { order: 8; }
.card-flow-form > .cf-col:nth-child(1) > :nth-child(5)  { order: 9; }
.card-flow-form > .cf-col:nth-child(2) > :nth-child(5)  { order: 10; }
.card-flow-form > .cf-col:nth-child(1) > :nth-child(6)  { order: 11; }
.card-flow-form > .cf-col:nth-child(2) > :nth-child(6)  { order: 12; }

/* ====== TABLET CARD FLOW (large-screen card layout) ======
   Opt a page's vertical stack of cards into a packed multi-column flow on
   tablet+ screens. Add `tablet-card-flow` to a wrapper around ONLY the page's
   cards — chrome (AppHeader, bottom bars, modals) stays full-bleed OUTSIDE it.

   Phone (<600px): the wrapper is a transparent passthrough — cards keep their
   own margins, so the phone layout is 100% untouched.

   Tablet+ (≥600px, the app's tablet threshold where displayScaleManager
   switches `zoom` to 1.2): cards flow into a centered, max-880px, ≤2-column
   masonry. `column-width: 280px` is tuned to that zoom so two columns fit from
   an iPad Mini (744px CSS) upward, while 7" tablets (~600px) stay a single
   centered column (two there would squeeze each card below a usable width).
   max-width caps it so it never spreads to 3 columns. Multi-column (vs a grid)
   packs masonry-style — a short card sits flush under the next instead of
   leaving a gap beside a tall one. (Derived + measured during the Posting
   rollout: layout engages at 600px, flips to two columns at ~730px.)

   Direct children are treated as cards: horizontal margins are stripped (they'd
   break the column track) and a uniform 16px bottom gap is applied. !important
   is required because each page's scoped per-card margin selector
   (`.card[b-xxxx]`, specificity 1,1,0) otherwise outranks this global utility. */
@media (min-width: 600px) {
    .tablet-card-flow {
        width: 100%;   /* stretch inside display:flex column parents (jf-form/jb-form);
                          no-op for block parents. Without it a flex item collapses to
                          one column. max-width still caps + margin auto centers. */
        max-width: var(--frame-content);
        margin: 0 auto;
        padding: 14px 16px 0;
        column-width: 280px;
        column-gap: 16px;
    }
    .tablet-card-flow > * {
        break-inside: avoid;
        margin: 0 0 16px !important;
    }

    /* Two real columns, each packing on its own (see .card-flow-form below).
       These selectors carry the extra `.tablet-card-flow` on purpose: the base
       .card-flow-form block sits FURTHER DOWN this file, so at equal specificity its
       `display: contents` would win here and the columns would never form. */
    .tablet-card-flow.card-flow-form {
        columns: auto;
        flex-direction: row;
        align-items: flex-start;
        column-gap: 16px;
    }
    .tablet-card-flow.card-flow-form > .cf-col {
        display: flex;
        flex-direction: column;
        flex: 1 1 0;
        min-width: 0;
    }
    /* The rule above strips card margins on the flow's direct children, which on this
       variant are the wrappers. The cards sit one level deeper, so they need it here.
       Only inside this media query: below it the wrappers are display:contents and the
       cards must keep the page's own phone margins untouched. */
    .tablet-card-flow.card-flow-form > .cf-col > * {
        break-inside: avoid;
        margin: 0 0 16px !important;
    }
}


/* ====== FRAME WIDTH (single source of truth) ======
   --frame-content = the wide-screen page content column; --frame-chrome (= content
   − 2×16 side padding) = header/composer content so it lines up with the cards.
   .app-framed / .app-framed-chrome centre + cap any element to those widths — the
   class form used by AuthLayout pages that don't get the .scroll-area cap (applied
   via markup, same idiom as .tablet-card-flow). Self-limiting: a no-op below the cap. */
:root {
    --frame-content: 880px;
    --frame-chrome: 848px;
}
.app-framed {
    width: 100%;
    max-width: var(--frame-content);
    margin-left: auto;
    margin-right: auto;
}
.app-framed-chrome {
    width: 100%;
    max-width: var(--frame-chrome);
    margin-left: auto;
    margin-right: auto;
}

/* The "receding modal" page blur is NOT here. It lives with the component that owns the
   element it blurs: AppShell.razor.css (Shared), together with the rule that pauses
   animations behind an open sheet, which is what keeps that blur's raster cached.
   Nothing about modal appearance belongs in this stylesheet.

   One consequence worth knowing while reading layouts: while a sheet is open the filter makes
   .app-blur-target the containing block for its position:fixed descendants (nav, headers).
   They are covered by the scrim so there is no visible shift, and it is gone the moment the
   scrim leaves the DOM. It is also why CameraOverlayHost, ToastContainer and CallManager are
   rendered OUTSIDE AppShell in every host: inside, the filter would trap them below the
   sheet. */

/* ====== WIDE-SCREEN CONTENT COLUMN ======
   Cap page content to the card column (880) and centre it so it lines up with
   the header + bottom-bar content (which cap to the same column). In MainLayout
   the header AND the page content are direct children of .scroll-area — so
   exclude .app-header (it stays full-bleed; only its inner nav caps) and centre
   every other child. GuestBodyBlur is a passthrough for logged-in users, so the
   content children sit directly here. Self-limiting (no effect below 880), so
   phones are unchanged. Global (not scoped) to reach the page content that Blazor
   renders into the layout's .scroll-area. */
.scroll-area > *:not(.app-header) {
    max-width: var(--frame-content);
    margin-left: auto;
    margin-right: auto;
}

/* ====== SCROLL FADE WRAP (reusable) ====== */
/* Usage: <div class="scroll-fade-wrap"><div class="scroll-content" @ref="ref">...</div></div> */
/* JS: enableDragScroll(ref) — auto-detects scroll-fade-wrap parent */
.scroll-fade-wrap {
    position: relative;
}

.scroll-fade-wrap::before,
.scroll-fade-wrap::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    width: 24px;
    pointer-events: none;
    z-index: 1;
    transition: opacity 0.2s;
}

.scroll-fade-wrap::before {
    left: 0;
    background: linear-gradient(to left, transparent, var(--scroll-fade-bg, var(--bg-page)));
}

.scroll-fade-wrap::after {
    right: 0;
    background: linear-gradient(to right, transparent, var(--scroll-fade-bg, var(--bg-page)));
}

/* Hide fade when scrolled to edge */
.scroll-fade-wrap.at-start::before { opacity: 0; }
.scroll-fade-wrap.at-end::after { opacity: 0; }

/* ====== IMAGE LIGHTBOX ======
   Shared zoom overlay for image previews across pages (Detail, ProfilTukang,
   SupportChat, Chat photo previews, etc). Tap overlay to close. */
.lightbox-overlay {
    position: fixed;
    inset: 0;
    /* Wide screens: cap + centre the viewer to the content column (inset:0 pins both
       sides, so max-width + margin-inline:auto centres it) so the frosted gutters stay
       visible on either side, matching the camera + framed pages. No-op below the cap. */
    max-width: var(--frame-content);
    margin-inline: auto;
    z-index: 9999;
    background: rgba(0,0,0,0.92);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    animation: lightbox-fade-in 0.3s ease;
}

@keyframes lightbox-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}
.lightbox-overlay img {
    max-width: 92%;
    max-height: 90vh;
    border-radius: 8px;
    object-fit: contain;
}
.lightbox-close {
    position: absolute;
    top: max(var(--safe-area-top), 12px);
    right: 12px;
    margin-top: 12px;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: rgba(255,255,255,0.15);
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
    border: none;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

/* Reply quote/bar UI is provided by <ChatReplyQuote /> and <ChatReplyBar />
   components in Components/ — see their scoped .razor.css files.
   Parent bubbles can still theme the quote by setting these CSS vars on an
   ancestor (they inherit through the DOM into the component's scope):
     --reply-quote-bg, --reply-line-color, --reply-name-color, --reply-text-color */

/* Flash highlight when user taps a reply quote to jump to original message.
   Triggered by scrollToChatMessage() in chat-helpers.js across chat surfaces. */
@keyframes msg-flash {
    0%   { background: rgba(21, 101, 192, .25); }
    100% { background: transparent; }
}
.msg-highlight { animation: msg-flash 1.5s ease-out; border-radius: 12px; }

/* Small inline spinner (file upload in chat attach button, etc) */
.chat-spinner {
    width: 18px;
    height: 18px;
    border: 2px solid var(--border);
    border-top-color: var(--blue-primary);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
}

/* ====== DISPLAY SCALE (page-root height override) ======
   When displayScaleManager applies CSS `zoom` to <html>, the document
   renders at scale× larger but `100dvh` still measures the unzoomed
   viewport — so .app-page, body, and per-page roots overflow by
   (scale-1)× viewport, pushing the bottom nav off-screen. Override the
   known page-root containers to compute their height against the zoom,
   so visual height = computed × zoom = exactly viewport.

   --app-zoom defaults to 1 (no scale). Keep this list in sync when a new
   page-root class is added — Playwright snap test `ScaleInvestigation`
   verifies nav stays visible at scale 1.15. */
html, body, #app,
.login-page, .forgot-page, .reset-page, .verify-page,
.app-page, .tfa-page, .complete-page, .onboarding,
.contact-page, .chat-page, .detail-page,
.jb-page, .jdw-page, .jf-page, .jd-page,
.keamanan-page, .kj-page,
.notifikasi-page, .notiflist-page,
.payment-page, .result-page, .cash-result-page, .legal-page,
.pelamar-page, .posting-page, .wp-page,
.referral-page, .rep-page, .search-page, .settings-page, .sc-page,
.vktp-page, .wallet-page, .earn-page, .earnings-page,
.home-page, .aktivitas-page, .profil-page,
.jasaku-page, .lowongan-page, .pt-page,
.loading-screen {
    /* .loading-screen is the full-screen page-load spinner root (Home/Detail/
       Lowongan/Jasaku/Aktivitas etc.). It uses height:100dvh, so it must divide
       by the zoom too — otherwise the box overflows by (scale-1)× viewport and
       the flex-centred spinner sinks below the physical centre on zoomed (wider)
       devices, while narrow (zoom 1.0) devices look fine.
       !important is required because Blazor scoped CSS rewrites per-page
       selectors to `.xxx-page[b-XXXX]` (specificity 1,1,0), out-ranking our
       plain class selector. Accessibility override is the right use case
       for !important — explicit cross-cutting concern. */
    height: calc(100dvh / var(--app-zoom, 1)) !important;
}

/* ====== IN-BODY LOADING (PageLoading.razor) ======
   Fills the space left under the page's header rather than replacing the screen. See the
   component for why. Self-contained on purpose — every page that needed a spinner had
   hand-copied `.loading-screen` + `.loading-spinner` into its own scoped stylesheet, eleven
   times over, and this is meant to be the one definition that replaces them.

   Keep this BELOW the page-root rule above. That rule is a long comma-separated selector
   list whose LAST entry is `.loading-screen` — inserting anything between the list and its
   `{` severs the two, and every page root silently loses its zoom-corrected height. Which
   is exactly what happened once: the whole app then overflowed by (zoom-1)x viewport and it
   looked like a brand-new layout bug rather than a broken edit. */
.page-loading {
    flex: 1 1 auto;          /* fill the remaining height of a flex-column shell */
    min-height: 180px;       /* …and don't collapse when the parent isn't one */
    display: flex;
    align-items: center;
    justify-content: center;
}
.page-loading-spinner {
    width: 40px;
    height: 40px;
    border: 4px solid var(--border);
    border-top-color: var(--blue-primary);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}
/* The spin keyframes live in each page's scoped CSS today; declare it here too so this
   component works on a page that has no spinner of its own. Duplicate @keyframes with the
   same name and body are harmless — the last one wins and they are identical. */
@keyframes spin {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .page-loading-spinner { animation-duration: 2.4s; }
}

/* ====== BOTTOM SHEET ANIMATIONS ======
   Canonical slide-up + backdrop fade for any modal that renders as a
   bottom-anchored sheet (pickers, ContactChangeModal, LoginPromptModal,
   Chat payment sheet, etc). Use these instead of defining per-component
   @keyframes so all sheets feel the same.

       .sheet-element { animation: app-slide-up 0.25s ease-out; }
       .backdrop-element { animation: app-fade-in 0.2s ease-out; }

   Keep durations unified across the app — touching them here propagates
   everywhere. */
@keyframes app-slide-up {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
}
@keyframes app-slide-down {
    from { transform: translateY(0); }
    to   { transform: translateY(100%); }
}
@keyframes app-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes app-fade-out {
    from { opacity: 1; }
    to   { opacity: 0; }
}

/* ====== SLIDE-UP MODAL OFFSET ======
   Globally bumps --modal-bottom-offset to clear the bottom-nav (MainLayout
   pages) or a page-level bottom bar (.app-bottom-bar — Detail/Posting/forms/
   etc). On pages without either, the :root default (just safe-area inset) wins
   and modals flush to the screen bottom. Lives in utilities.css so it applies
   regardless of which scoped stylesheet owns the nav/bar element. */
/* safe-area-bottom-fixed (not raw) so the offset matches the bar height, which is
   also zoom-divided — otherwise a slide-up sheet floats a few px above the bar at
   scale >1. A page-level .app-bottom-bar is always flush to the screen edge, so
   its clearance is simply its height. */
body:has(.app-bottom-bar) {
    --modal-bottom-offset: calc(var(--nav-height) + var(--safe-area-bottom-fixed));
}

/* The bottom-nav's clearance is THEMED, because a theme may float the bar off the
   screen edge — and then its top edge is higher than its height alone implies.
   See --nav-clearance in ui-theme.css. */
body:has(.bottom-nav) {
    --modal-bottom-offset: var(--nav-clearance);
}

/* ====== NOTICE BOX ======
   Unified warning/info box: dark bg in dark mode, light orange/blue tint in
   light mode, bold colored title + body. Use for inline warnings, drafts,
   verification notices. Replaces ad-hoc .draft-notice / .kj-warning / .vo-warn
   / .vktp-failure styles previously duplicated across pages.

   Markup:
     <div class="notice-box">
       <div class="notice-box-title">Title</div>
       <div class="notice-box-msg">Body text.</div>
       <div class="notice-box-hint">Optional secondary hint.</div>
     </div>

   Variants: add .notice-box-info for blue (info), .notice-box-row for
   icon-on-left layout. Action button: nest .notice-box-action element. */
.notice-box {
    padding: 12px 14px;
    background: var(--orange-light, #FFF4E5);
    border: 1px solid var(--orange-badge, #F59E0B);
    border-radius: 10px;
    margin-bottom: 12px;
}
.notice-box-info {
    background: var(--blue-pale, #E3F2FD);
    border-color: var(--blue-primary, #1565C0);
}
.notice-box-title {
    font-weight: 600;
    font-size: 13px;
    color: var(--orange-badge, #F59E0B);
    margin-bottom: 4px;
}
.notice-box-info .notice-box-title {
    color: var(--blue-primary, #1565C0);
}
.notice-box-msg {
    font-size: 12px;
    color: var(--text-primary);
    line-height: 1.5;
}
.notice-box-hint {
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.4;
    margin-top: 6px;
}
.notice-box-row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
}
.notice-box-row > svg {
    flex-shrink: 0;
    margin-top: 1px;
    color: var(--orange-badge, #F59E0B);
}
.notice-box-info .notice-box-row > svg {
    color: var(--blue-primary, #1565C0);
}
.notice-box-row > .notice-box-body {
    flex: 1 1 auto;
    min-width: 0;
}
.notice-box-action {
    margin-top: 10px;
    background: transparent;
    border: 1px solid var(--orange-badge, #F59E0B);
    border-radius: 8px;
    padding: 6px 12px;
    font-size: 12px;
    font-weight: 700;
    color: var(--orange-badge, #F59E0B);
    cursor: pointer;
}
.notice-box-info .notice-box-action {
    border-color: var(--blue-primary, #1565C0);
    color: var(--blue-primary, #1565C0);
}

/* Dropdown (ListboxMenu) reveal fallback — global because @keyframes names are
   global and this sidesteps scoped-CSS @-rule quirks. The menu is placed + revealed
   by lbPlaceMenu; this only fires if that JS never runs, so the menu can't stay
   invisible. See ListboxMenu.razor.css (.lb-menu / .lb-ready). */
@keyframes lb-menu-reveal { to { visibility: visible; } }

/* ====== PWA INSTALL PROMPT (web only; injected by pwa-install.js) ======
   Dismissible floating card offering to install the app. Sits above the bottom
   nav via a fixed bottom offset; theme tokens adapt it to light/dark. */
.pwa-install {
    position: fixed;
    left: 50%;
    bottom: calc(env(safe-area-inset-bottom, 0px) + 84px);
    transform: translateX(-50%) translateY(16px);
    z-index: 9000;
    width: min(420px, calc(100% - 24px));
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    background: var(--bg-white);
    border: 1px solid var(--card-edge-color, var(--border));
    border-radius: var(--card-radius, 14px);
    box-shadow: 0 8px 28px rgba(13, 33, 55, .20);
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s ease, transform .2s ease;
}
.pwa-install.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    pointer-events: auto;
}
.pwa-install-icon { width: 40px; height: 40px; border-radius: 10px; flex-shrink: 0; }
.pwa-install-body { flex: 1; min-width: 0; }
.pwa-install-title { font-size: 14px; font-weight: 700; color: var(--text-primary); }
.pwa-install-sub { font-size: 12px; line-height: 1.35; color: var(--text-muted); }
.pwa-install-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.pwa-install-btn {
    padding: 8px 14px; border: none; border-radius: 10px;
    font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; color: #fff;
    background: linear-gradient(145deg, var(--btn-primary-start), var(--btn-primary-end));
}
.pwa-install-dismiss {
    padding: 8px 10px; border: none; background: none; cursor: pointer;
    font-size: 13px; font-weight: 600; font-family: inherit; color: var(--text-muted);
}

/* ====== HEADER ICON BUTTON + NOTIFICATION BADGE ====== */
.icon-btn {
    width: 40px;
    height: 40px;
    /* Themed: this is the header's action button (Beranda's bell, Lowongan's
       filter), so it follows --header-control-radius rather than pinning a
       literal — otherwise it stays a rounded square beside a circular back
       button whenever the theme makes header controls circular. */
    border-radius: var(--header-control-radius);
    border: none;
    background: rgba(255,255,255,0.12);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    cursor: pointer;
    transition: background 0.2s;
}

/* Interaction states — sits on the blue header, so translucent-white bumps.
   Press (:active) + keyboard focus light it up; hover only on pointer devices
   so it doesn't stick after a tap on touch. */
.icon-btn:active,
.icon-btn:focus-visible { background: rgba(255,255,255,0.24); }
@media (hover: hover) {
    .icon-btn:hover { background: rgba(255,255,255,0.18); }
}

/* ====== PLAIN ICON BUTTON (light headers) ======
   The counterpart to .icon-btn above for the app's WHITE headers — the chat pages.
   .icon-btn's recipe cannot be reused there: a 12%-white fill and a #fff icon are both
   invisible on white. So: no fill, and hover/focus BOTH light the icon blue (the core
   rule — one cue for mouse and keyboard alike). `outline:none` suppresses the app-wide
   button:focus-visible fallback ring, which would otherwise be a SECOND, different focus
   look; 0,2,0 beats that fallback's 0,1,1.

   Idle is --text-secondary rather than the --text-muted of in-field icon buttons
   (.search-clear, .password-toggle): those are field ornaments, this is the way out of
   the page, so it has to read at a glance. (--text-muted was #9EAAB6 / 2.37:1 when this
   was written, which made the gap obvious; it is #67737F / 4.84:1 now, so the two are
   closer — the distinction here is emphasis, not legibility.)

   Global, not per-page, on purpose: /chat and /support-chat are hand-copies of one
   header, and their back buttons drifted apart the moment one was touched. One class,
   one definition — a third copy can't fork it. Any page-scoped `.x[b-…]` re-declaring
   these properties would OUTRANK this file, so style the button here, not there. */
.icon-btn-plain {
    width: 36px;
    height: 36px;
    border: none;
    background: none;
    color: var(--text-secondary);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    flex-shrink: 0;
}

.icon-btn-plain:hover,
.icon-btn-plain:focus-visible { color: var(--blue-primary); outline: none; }

/* ====== CHAT CONTEXT CARD ======
   The "what is this conversation about" card under a white chat header: /chat shows the
   job, /support-chat shows the ticket. Both used to be the same full-bleed --blue-pale bar
   with blue text on blue — square-cornered, flush against the header's bottom border —
   because one was hand-copied from the other. Shared here for the same reason as
   .icon-btn-plain above: every copied piece of these two headers has drifted.

   Shared: the shell, the icon box, the title/meta column. Per page: whether it's a <button>
   (Chat's card opens /detail) or a plain <div> (a ticket has nowhere to go), and the glyph.
   A scoped `.chat-ctx[b-…]` would outrank this file — style it here, not there.

   The card needs its border to exist at all: --bg-white is the SAME colour as the header
   behind it in BOTH themes (#FFFFFF on #FFFFFF light, #1A1E24 on #1A1E24 dark). --bg-page
   wouldn't save it either — in light that's #FAFAFA, a 2% difference nobody sees. So the
   1px --border draws it and --shadow-sm lifts it. Capped to --frame-chrome so its edges
   line up with the header row above; the header's own 16px padding insets it on phones.

   Shape, fill, tile and arrow are the --ctx-* tokens (ui-theme.css): classic is the
   bordered white card described above; capsule sinks it into a trough and stands the
   tile and the arrow on it as raised discs. The reasoning is beside the tokens. */
.chat-ctx {
    display: flex;
    width: 100%;
    max-width: var(--frame-chrome);
    margin: 0 auto 12px;
    text-align: left;
    align-items: center;
    gap: 10px;
    padding: var(--ctx-pad);
    background: var(--ctx-fill);
    border: 1px solid var(--card-edge-color, var(--border));
    border-radius: var(--ctx-radius);
    box-shadow: var(--ctx-edge);
}

/* The one accent on an otherwise neutral card — this is where the blue that used to wash
   the whole bar now lives. --blue-pale is also the only fill that reads on white AND in
   dark (#E8F1FB / #1A2D42); a --bg-white box would vanish into the card. */
.chat-ctx-i {
    width: var(--ctx-tile-size);
    height: var(--ctx-tile-size);
    border-radius: var(--ctx-tile-radius);
    background: var(--ctx-tile-bg);
    box-shadow: var(--ctx-tile-shadow);
    color: var(--blue-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* The glyph's box and depth are themed here rather than by the Icon's Size, so the
   call site keeps one Size for both themes: classic derives its line weight from
   Size and would thicken if the attribute changed, while capsule's solid glyph can
   simply be scaled by CSS. */
.chat-ctx-i .ic {
    width: var(--ctx-icon-size);
    height: var(--ctx-icon-size);
    filter: var(--ctx-icon-shadow);
}

.chat-ctx-t { flex: 1; min-width: 0; }

/* No blue-on-blue: on a neutral card these read as ordinary card text, like a product row. */
.chat-ctx-tt {
    font-size: 12.5px;
    font-weight: 700;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.chat-ctx-mt {
    font-size: 12px;
    font-weight: 500;
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Clickable variant only. Canonical clickable-card cue, spelled out rather than left to
   .card-clickable: .btn-plain:focus-visible also sets box-shadow and both those globals
   score 0,2,0, so source order alone would pick the winner. `button.chat-ctx` is 0,2,1 and
   settles it. The non-clickable <div> in /support-chat gets no cue, which is correct — it
   isn't a control. */
button.chat-ctx { cursor: pointer; transition: transform .2s, box-shadow .2s, background .2s; }

button.chat-ctx:hover,
button.chat-ctx:focus-visible {
    outline: none;
    background: var(--ctx-hover-fill);
    transform: var(--ctx-hover-transform);
    box-shadow: var(--ctx-hover-edge);
}

/* Was a hardcoded #4A90D9 in Chat (§3 bans raw hex); currentColor + a token keeps the
   "open" arrow themed in both modes. A box around the arrow only under capsule, where
   it is a raised disc on the trough; classic's auto size and transparent fill leave it
   the bare glyph it was. */
.chat-ctx-go {
    color: var(--blue-primary);
    flex-shrink: 0;
    width: var(--ctx-go-size);
    height: var(--ctx-go-size);
    border-radius: var(--r-circle, 50%);
    background: var(--ctx-go-fill);
    box-shadow: var(--ctx-go-shadow);
    display: flex;
    align-items: center;
    justify-content: center;
}

.notif-badge {
    position: absolute;
    top: 6px;
    right: 4px;
    min-width: 14px;
    height: 14px;
    padding: 0 3px;
    background: #EF4444;
    /* A count badge holds a short label, so it is a pill. Zero classic change:
       at 14px tall the old 7px was already half the box. */
    border-radius: var(--r-pill);
    font-size: 8px;
    font-weight: 700;
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

/* ====== SAFE AREA ====== */
.status-bar-safe-area {
    display: none;
}

@supports (-webkit-touch-callout: none) {
    .status-bar-safe-area {
        display: flex;
        position: sticky;
        top: 0;
        height: env(safe-area-inset-top);
        background-color: var(--header-grad-start);
        width: 100%;
        z-index: 1;
    }
}
