/* ============================================
   TRALENSO UTILITIES
   Shared utility classes used across the app.
   
   TABLE OF CONTENTS:
   1. Layout & Page Structure
   2. Grid Systems
   3. Typography & Headers
   4. Buttons
   5. Form Elements (Inputs, Textareas, Validation)
   6. Character Counter
   7. File Upload
   8. Checkboxes
   9. Categories & Icons
   10. Smart Filters
   11. Reviews & Ratings
   12. Modals
   13. UI States (Empty, Loading, Badges)
   14. Text Utilities
   15. Decorative / Borders
   ============================================ */

/* ============================================
   BREAKPOINT REFERENCE (Bootstrap 5 Standard)
   ============================================ */

@media (max-width: 767px) {
    :root {
        --nav-height: 5.6rem;
    }
}

@media (max-width: 575px) {
    /* 5.6rem, not 5rem. This bar contains the search field, and a 44px touch target
       cannot live inside a 50px bar — it left 2px of clearance and the input looked
       like it WAS the nav. Matches the 767px value; the two ranges only differed
       because nothing tall used to sit in here. */
    :root {
        --nav-height: 5.6rem;
    }
}

/* ============================================
   1. LAYOUT & PAGE STRUCTURE
   ============================================ */
.default-page {
    display: flex;
    flex-direction: column;
    background: var(--color-background-light);
}

/* Full-bleed by default — the sidebar is chrome, so it hugs the window edge the way
   Facebook's does. .default-page__content inside it still caps at --page-max-width and
   centres in the remaining space, which is the part that actually needs constraining.
   Capping the grid itself letterboxed the whole page at 1920, squeezing the content
   column to ~1430px for no benefit.
   The cap returns above 2200px — see the min-width block below. */
.default-page__layout {
    display: grid;
    grid-template-columns: var(--sidebar-w) 1fr;
    width: 100%;
}

    .default-page__layout > * {
        min-width: 0;
    }

.default-page__sidebar {
    height: 100%;
    width: var(--sidebar-w);
}

.default-page__content {
    grid-column-start: 2;
    min-width: 0;
    margin: 0 auto;
    max-width: var(--page-max-width);
    padding-top: var(--nav-height);
    padding-bottom: var(--page-bottom-gap);
    padding-left: 2rem;
    padding-right: 2rem;
    width: 100%;
}

.default-page__content_banner {
    grid-column-start: 2;
    min-width: 0;
    margin: 0 auto;
    max-width: var(--page-max-width);
    padding-top: 2rem;
    padding-bottom: var(--page-bottom-gap);
    padding-left: 2rem;
    padding-right: 2rem;
    width: 100%;
}

.default-page__content_tab {
    margin: 0 auto;
    max-width: var(--page-max-width);
    padding-left: 4rem;
    padding-right: 4rem;
    width: 100%;
}

.default-page__content_tab-sidebar {
    margin: 0 auto;
    max-width: var(--page-max-width);
    padding-left: 0rem;
    padding-right: 0rem;
    width: 100%;
}

.default_page-content-height {
    margin-top: 3rem;
}

/* The content column on every sidebar page (MyLibrary, Profile, GuideCreate) and the
   whole column on sidebar-less ones (GuideViewer). Always a sibling of
   .default-page__sidebar, never an ancestor — which is exactly why the bottom gap
   belongs here rather than on .default-page. */
.default-home {
    padding-top: var(--nav-height);
    padding-bottom: var(--page-bottom-gap);
}

.default-home__content {
    grid-column-start: 2;
    min-width: 0;
    margin: 0 auto;
    max-width: var(--page-max-width);
    padding-top: var(--nav-height);
    padding-left: 2rem;
    padding-right: 2rem;
    width: 100%;
}

@media (max-width: 1535px) {
    .default-page__content_banner {
        padding-left: 3rem;
        padding-right: 3rem;
    }

    .default-page__content {
        padding-left: 4rem;
        padding-right: 4rem;
    }


    .default-page__content_tab {
        padding-left: 0rem;
        padding-right: 0rem;
    }
}

/* ULTRA-WIDE — the one min-width query in this file, and deliberately so.
   Below 2200px a flush-left sidebar reads as intentional. Beyond it the gap between the
   sidebar and the capped content column grows large enough that the two stop looking
   like one page, so the whole layout re-centres. 2200 rather than --page-max-width
   (1670) because 1670 catches every 1920px monitor — the common case, where flush-left
   is what we want. Nothing standard sits at 2200; the next real step up is 2560. */
@media (min-width: 2200px) {
    .default-page__layout {
        max-width: var(--page-max-width);
        margin: 0 auto;
    }
}

@media (max-width: 1279px) {    
}

/* Sidebar collapse — tablet and below */
@media (max-width: 1023px) {
    /* No padding-bottom here. The bottom nav doesn't render above 767px, so this range
       needs no clearance — and below 767px it stacked on top of .default-page__content's
       own clearance, giving creator/admin pages ~14rem of dead space. One source of
       truth: .default-page__content / .default-home in the 767px block. */
    .default-page__layout {
        grid-template-columns: var(--sidebar-w-collapsed) 1fr;
    }

    .default-page__sidebar {
        width: var(--sidebar-w-collapsed);
    }

    .default-page__content {
        padding-left: 2rem;
        padding-right: 2rem;
    }

    .default-page__content_banner {
        padding-top: 1.5rem;
        padding-left: 2rem;
        padding-right: 2rem;
    }

    .default_page-content-height {
        margin-top: 2rem;
    }
}

@media (max-width: 767px) {
    /* 7.6rem, not 6rem: the bottom nav is a FLOATING pill now, so clearance is the
       pill's own height (5.6rem) plus the 1rem it sits above the safe area, plus 1rem
       of breathing room. At 6rem the last card tucks under the glass. */
    .default-page__content {
        padding-bottom: calc(7.6rem + env(safe-area-inset-bottom));
    }

    /* Same bottom-nav clearance as .default-page__content above. Needed separately
       because tab-style pages (GuideViewer) wrap their content in .default-home
       rather than .default-page__content, so they got no clearance at all and the
       last card sat under the fixed .bottom-nav.

       Note this does NOT belong on body: variables.css is linked a second time
       AFTER app.css and its `body { padding: 0 }` shorthand zeroes out the
       `body { padding-bottom }` rule in TopNavigationBar.css. Until that duplicate
       <link> is removed, per-container clearance is the only thing that survives. */
    .default-home {
        padding-bottom: calc(7.6rem + env(safe-area-inset-bottom));
    }

    .default-page__layout {
        grid-template-columns: 1fr;
    }

    .default-page__sidebar {
        display: none;
    }

    .default-page__content {
        grid-column-start: 1;
    }

    .default_page-content-height {
        margin-top: 1.5rem;
    }

    .default-home__content {
        padding-top: 0rem;
    }
}


/* ============================================
   2. GRID SYSTEMS
   ============================================ */

.column-2 {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
}

.column-3 {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
}

.column-4 {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
}

.column-6 {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
}


/* ============================================
   3. TYPOGRAPHY & HEADERS

   Sizes are fluid rather than stepped. Every clamp below is DERIVED from the four
   breakpoint blocks this replaces — floor = the old 575px value, ceiling = the old
   base value, slope fitted between 375px and 1920px. The fitted curves pass through
   the old 1023px numbers almost exactly (title: 16.5px computed vs 1.65rem declared),
   so this is the same type scale without the steps.

   Why this is worth doing beyond the line count: a stepped scale jumps 3px at a
   breakpoint, so a heading visibly resizes while a user rotates a tablet or drags a
   window. A clamp just tracks the viewport.

   TWO DELIBERATE DEPARTURES from the old values, both flagged inline below:
     • sub-11.5px mobile sizes raised — see the floors on description / sub-description
     • description-2 aligned with description at small mobile — it had no 575px rule
   ============================================ */

.tralenso-guide-header-content {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    /* space-between with no gap let a long title butt straight into whatever sits on
       the right (usually an action button) once the row got tight. */
    gap: 1.6rem;
    margin-bottom: 1rem;
}

.tralenso-content__header {
    display: flex;
    flex-direction: column;
}

/* ── Shared wrapping behaviour ──────────────────────────────────────────────────
   `word-break: break-word` was declared on all eight classes below. It's a legacy
   alias that does nothing overflow-wrap isn't already doing, and it's non-standard —
   the spec lists it as deprecated. Grouped here so the eight rules stay readable. */
.tralenso-header__title,
.tralenso-header__title-2,
.tralenso-header__sub-title,
.tralenso-header__sub-title-2,
.tralenso-header__sub-title-3,
.tralenso-header__description,
.tralenso-header__description-2,
.tralenso-header__sub-description {
    overflow-wrap: break-word;
}

/* Titles */
.tralenso-header__title {
    color: var(--color-dark-text);
    /* 1.4rem @375px → 2rem @1920px. Computes to 16.5px at 1023px, matching the old
       1.65rem step exactly. */
    font-size: clamp(1.4rem, 1.25rem + 0.39vw, 2rem);
    font-weight: 600;
    /* Headings inherited body's 1.6, which on a 20px title is 32px of leading — a
       two-line guide title read as two separate lines rather than one heading.
       1.25 is the standard display value. */
    line-height: 1.25;
    /* Evens out the last line instead of leaving one orphaned word. Ignored by
       engines that don't support it, so it costs nothing. */
    text-wrap: balance;
}

.tralenso-header__title-2 {
    display: flex;
    flex-direction: row;
    gap: 0.8rem;
    align-items: center;
    color: var(--color-dark-text);
    font-size: clamp(1.25rem, 1.09rem + 0.42vw, 1.9rem);
    font-weight: 500;
    line-height: 1.3;
}

/* Sub-Titles */
.tralenso-header__sub-title {
    color: var(--color-dark-text);
    /* Floor raised from 1.15rem (11.5px) to 1.25rem. 11.5px is below the size at
       which text is comfortably readable on a phone held at arm's length, and this
       class labels sections — it's the text that tells someone where they are.
       Revert to 1.15rem if you disagree; only the first value changes. */
    font-size: clamp(1.25rem, 1.1rem + 0.29vw, 1.6rem);
    font-weight: 500;
    line-height: 1.35;
}

.tralenso-header__sub-title-2 {
    color: var(--color-dark-text);
    font-size: clamp(1.3rem, 1.18rem + 0.32vw, 1.8rem);
    font-weight: 500;
    align-self: center;
    /* Fluid too, so the last remaining media query in this section could go. Old
       values were 1rem base / 0.5rem at 575px — this reproduces both endpoints. */
    margin-right: clamp(0.5rem, 1vw, 1rem);
    line-height: 1.35;
    margin-bottom: 0;
}

.tralenso-header__sub-title-3 {
    color: var(--color-dark-text);
    /* Floor raised from 1.1rem (11px) — same reasoning as sub-title above. */
    font-size: clamp(1.15rem, 1.09rem + 0.16vw, 1.4rem);
    font-weight: 500;
    align-self: center;
    margin-right: clamp(1rem, 2vw, 2rem);
    line-height: 1.35;
    margin-bottom: 0;
}

/* Descriptions */
.tralenso-header__description {
    color: var(--color-gray-text);
    /* Floor raised from 1.1rem (11px) to 1.25rem. This is body copy — the paragraph
       under a heading — and 11px is smaller than the smallest text anywhere else in
       the app, including the badges and the char counter. */
    font-size: clamp(1.25rem, 1.17rem + 0.23vw, 1.6rem);
    font-weight: 300;
    /* Prose keeps generous leading; only the headings above tighten. */
    line-height: 1.55;
}

.tralenso-header__description-2 {
    color: var(--color-gray-text);
    /* This class had NO 575px rule, so at small mobile it stayed at 1.3rem while
       .tralenso-header__description — used interchangeably with it — dropped to
       1.1rem. The two now share a floor. */
    font-size: clamp(1.25rem, 1.19rem + 0.16vw, 1.5rem);
    font-weight: 300;
    line-height: 1.55;
}

.tralenso-header__sub-description {
    color: var(--color-gray-text);
    /* Floor raised from 1.1rem (11px). */
    font-size: clamp(1.2rem, 1.15rem + 0.13vw, 1.4rem);
    font-weight: 300;
    line-height: 1.5;
}

/* ── The four breakpoint blocks that used to live here are gone ─────────────────
   ~90 lines replaced by the clamps above. If a specific page needs a size that
   doesn't fit the scale, override that class in that page's own stylesheet rather
   than reintroducing a global breakpoint here — that's what let the old blocks drift
   (description-2 never got its 575px entry, and nobody noticed for a long time).

   The HEIGHT-CONSTRAINED block in variables.css still applies: it tightens
   --fs-display and --fs-h1 on short screens. These classes don't read those tokens
   yet, and deliberately so — merging the two scales is a separate decision, not
   something to slip into a visual pass. */

@media (max-width: 767px) {
    /* Wraps once the row gets tight instead of crushing the title against the button
       beside it. Kept as a query rather than `flex-wrap: wrap` globally, because on
       desktop the two should stay on one line even when the title is long. */
    .tralenso-guide-header-content {
        flex-wrap: wrap;
        gap: 0.8rem;
    }
}

/* ============================================
   4. BUTTONS

   Interaction model (same as the nav bars):
     • hover is pointer-gated so a tap can't leave a button stuck highlighted
     • :active carries the feedback that touch actually gets — a scale-down press
     • gradients brighten with filter, they never get swapped for a flat colour
     • only transform / filter / colour animate, so nothing reflows mid-transition
   ============================================ */

/* Shared across every button in this section. Grouped rather than repeated so the
   press feel is one edit, and so a new variant can't be added without it.
   :focus-visible (not :focus) — a keyboard ring on every mouse click looks broken. */
.tralenso-primary-btn,
.tralenso-primary-btn-sm,
.tralenso-secondary-btn,
.tralenso-secondary-btn-sm,
.tralenso-remove-btn,
.tralenso-remove-btn-sm,
.btn-icon-primary,
.btn-icon-primary-m,
.btn-icon-primary-sm,
.btn-icon-trash,
.btn-icon-trash-m,
.btn-icon-trash-sm {
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}

    .tralenso-primary-btn:focus-visible,
    .tralenso-primary-btn-sm:focus-visible,
    .tralenso-secondary-btn:focus-visible,
    .tralenso-secondary-btn-sm:focus-visible,
    .tralenso-remove-btn:focus-visible,
    .tralenso-remove-btn-sm:focus-visible,
    .btn-icon-primary:focus-visible,
    .btn-icon-primary-m:focus-visible,
    .btn-icon-primary-sm:focus-visible,
    .btn-icon-trash:focus-visible,
    .btn-icon-trash-m:focus-visible,
    .btn-icon-trash-sm:focus-visible {
        outline: 0.2rem solid var(--color-primary);
        outline-offset: 0.2rem;
    }

/* Primary Buttons */
.tralenso-primary-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.8rem 1.6rem;
    background: var(--gradient-primary);
    color: white;
    border: none;
    border-radius: 1rem;
    font-size: 1.5rem;
    gap: 0.5rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    /* Tinted in the brand colour rather than neutral black. A grey shadow under an
       orange button reads as dirt; a warm one reads as the button glowing. */
    box-shadow: 0 0.2rem 0.6rem rgba(255, 149, 0, 0.25);
    transition: filter 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}

    /* 0.96, not 1.01. The old value was a 1% change — mathematically a scale, visually
       nothing. A press has to be felt, and pressing IN is what every native control does. */
    .tralenso-primary-btn:active {
        transform: scale(0.96);
        box-shadow: 0 0.1rem 0.3rem rgba(255, 149, 0, 0.2);
    }

.tralenso-primary-btn-sm {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem 1.2rem;
    background: var(--gradient-primary);
    color: white;
    border: none;
    border-radius: 0.8rem;
    font-size: 1.2rem;
    gap: 0.5rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    box-shadow: 0 0.15rem 0.4rem rgba(255, 149, 0, 0.22);
    transition: filter 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}

    .tralenso-primary-btn-sm:active {
        transform: scale(0.95);
        box-shadow: 0 0.1rem 0.2rem rgba(255, 149, 0, 0.18);
    }

@media (hover: hover) and (pointer: fine) {
    /* filter, not a background swap. The old rule replaced --gradient-primary with a
       flat --color-primary-dark, which (a) killed the gradient and (b) can't transition,
       because `background` wasn't in the transition list — so it snapped. brightness
       animates cleanly and keeps the gradient. Same fix already applied to .btn-login. */
    .tralenso-primary-btn:hover,
    .tralenso-primary-btn-sm:hover {
        filter: brightness(1.08);
        color: white;
        transform: translateY(-0.1rem);
    }

    .tralenso-primary-btn:hover {
        box-shadow: 0 0.4rem 1.2rem rgba(255, 149, 0, 0.32);
    }

    .tralenso-primary-btn-sm:hover {
        box-shadow: 0 0.3rem 0.8rem rgba(255, 149, 0, 0.28);
    }
}

/* Cancel Buttons */
.tralenso-secondary-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.8rem 1.6rem;
    /* Hairline in a translucent black instead of the solid --color-gray-text (#7C7C7C).
       At full strength the outline competed with the primary button beside it; a tint
       also sits correctly on both the light page and a white card. */
    background: transparent;
    color: var(--color-dark-gray-text);
    border: 0.1rem solid rgba(0, 0, 0, 0.14);
    border-radius: 1rem;
    gap: 0.5rem;
    font-size: 1.5rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}

    .tralenso-secondary-btn:active {
        background: rgba(0, 0, 0, 0.06);
        transform: scale(0.96);
    }

.tralenso-secondary-btn-sm {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem 1.2rem;
    background: transparent;
    color: var(--color-dark-gray-text);
    border: 0.1rem solid rgba(0, 0, 0, 0.14);
    border-radius: 0.8rem;
    gap: 0.5rem;
    font-size: 1.2rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}

    .tralenso-secondary-btn-sm:active {
        background: rgba(0, 0, 0, 0.06);
        transform: scale(0.95);
    }

@media (hover: hover) and (pointer: fine) {
    /* rgba, not --color-background-light. That token is #FAFAFA — the page background —
       so on almost every screen the old hover filled the button with the colour already
       behind it and nothing visibly happened. */
    .tralenso-secondary-btn:hover,
    .tralenso-secondary-btn-sm:hover {
        background: rgba(0, 0, 0, 0.04);
        color: var(--color-dark-text);
        border-color: rgba(0, 0, 0, 0.24);
    }
}

/* Destructive Buttons */
.tralenso-remove-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.8rem 1.6rem;
    background: transparent;
    color: var(--color-danger);
    border: 0.1rem solid rgba(239, 68, 68, 0.45);
    border-radius: 1rem;
    gap: 0.5rem;
    font-size: 1.5rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}

    .tralenso-remove-btn:active {
        background: rgba(239, 68, 68, 0.14);
        transform: scale(0.96);
    }

.tralenso-remove-btn-sm {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem 1.2rem;
    background: transparent;
    color: var(--color-danger);
    border: 0.1rem solid rgba(239, 68, 68, 0.45);
    border-radius: 0.8rem;
    gap: 0.5rem;
    font-size: 1.2rem;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.15s ease;
}

    .tralenso-remove-btn-sm:active {
        background: rgba(239, 68, 68, 0.14);
        transform: scale(0.95);
    }

@media (hover: hover) and (pointer: fine) {
    /* rgba over the #fee2e2 literal so the tint composites onto whatever is behind the
       button — a white card, the page, or a modal — instead of punching an opaque pink
       rectangle through it. */
    .tralenso-remove-btn:hover,
    .tralenso-remove-btn-sm:hover {
        background: rgba(239, 68, 68, 0.09);
        color: var(--color-dark-red);
        border-color: var(--color-danger);
    }
}

/* ── Icon buttons ───────────────────────────────────────────────────────────────
   These had NO transition at all, so every hover was an instant colour snap, and
   none of them had a pressed state — on touch they gave no feedback whatsoever.
   Grouped by size below; only padding, radius and glyph size differ. */

.btn-icon-primary,
.btn-icon-primary-m,
.btn-icon-primary-sm,
.btn-icon-trash,
.btn-icon-trash-m,
.btn-icon-trash-sm {
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    cursor: pointer;
    transition: filter 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}

    /* 0.9 — icon buttons are small, so they need a deeper squash than a text button
       for the press to register at the same perceived strength. */
    .btn-icon-primary:active,
    .btn-icon-primary-m:active,
    .btn-icon-primary-sm:active,
    .btn-icon-trash:active,
    .btn-icon-trash-m:active,
    .btn-icon-trash-sm:active {
        transform: scale(0.9);
    }

@media (hover: hover) and (pointer: fine) {
    .btn-icon-primary:hover,
    .btn-icon-primary-m:hover,
    .btn-icon-primary-sm:hover,
    .btn-icon-trash:hover,
    .btn-icon-trash-m:hover,
    .btn-icon-trash-sm:hover {
        filter: brightness(1.1);
        transform: translateY(-0.1rem);
    }

    .btn-icon-primary:hover,
    .btn-icon-primary-m:hover,
    .btn-icon-primary-sm:hover {
        box-shadow: 0 0.3rem 0.8rem rgba(255, 149, 0, 0.3);
    }

    .btn-icon-trash:hover,
    .btn-icon-trash-m:hover,
    .btn-icon-trash-sm:hover {
        box-shadow: 0 0.3rem 0.8rem rgba(204, 33, 38, 0.3);
    }
}

/* Icon glyphs — one rule per size instead of three near-identical blocks.
   position: relative dropped: nothing was positioned against it. */
.btn-icon-primary img,
.btn-icon-trash img {
    width: 2.3rem;
    height: 2.3rem;
    filter: brightness(0) invert(1);
}

.btn-icon-primary-m img,
.btn-icon-trash-m img {
    width: 2rem;
    height: 2rem;
    filter: brightness(0) invert(1);
}

.btn-icon-primary-sm img,
.btn-icon-trash-sm img {
    width: 1.6rem;
    height: 1.6rem;
    filter: brightness(0) invert(1);
}

/* Primary icon buttons — gradient, matching .tralenso-primary-btn. The flat
   --color-primary made them look like a different family from the text buttons
   they usually sit next to. */
.btn-icon-primary {
    padding: 0.6rem;
    border-radius: 0.8rem;
    background: var(--gradient-primary);
    box-shadow: 0 0.15rem 0.4rem rgba(255, 149, 0, 0.22);
}

.btn-icon-primary-m {
    padding: 0.6rem;
    border-radius: 0.7rem;
    background: var(--gradient-primary);
    box-shadow: 0 0.15rem 0.4rem rgba(255, 149, 0, 0.22);
}

.btn-icon-primary-sm {
    padding: 0.5rem;
    border-radius: 0.7rem;
    background: var(--gradient-primary);
    box-shadow: 0 0.15rem 0.4rem rgba(255, 149, 0, 0.22);
}

/* Destructive icon buttons. #CC2126 kept as the gradient's top stop so the resting
   colour is unchanged from before — only the finish is new. Worth promoting to a
   token (--gradient-danger) when you next touch variables.css. */
.btn-icon-trash {
    padding: 0.6rem;
    border-radius: 0.8rem;
    background: linear-gradient(180deg, #CC2126 0%, #a30000 100%);
    box-shadow: 0 0.15rem 0.4rem rgba(204, 33, 38, 0.22);
}

.btn-icon-trash-m {
    padding: 0.6rem;
    border-radius: 0.7rem;
    background: linear-gradient(180deg, #CC2126 0%, #a30000 100%);
    box-shadow: 0 0.15rem 0.4rem rgba(204, 33, 38, 0.22);
}

.btn-icon-trash-sm {
    padding: 0.5rem;
    border-radius: 0.7rem;
    background: linear-gradient(180deg, #CC2126 0%, #a30000 100%);
    box-shadow: 0 0.15rem 0.4rem rgba(204, 33, 38, 0.22);
}

    /* Disabled States — now covers the icon buttons too. Previously only
   .btn-icon-primary-sm had a :disabled rule, so a disabled .btn-icon-trash still
   looked and behaved fully enabled. */
    .tralenso-primary-btn:disabled,
    .tralenso-primary-btn-sm:disabled,
    .tralenso-secondary-btn:disabled,
    .tralenso-secondary-btn-sm:disabled,
    .tralenso-remove-btn:disabled,
    .tralenso-remove-btn-sm:disabled,
    .btn-icon-primary:disabled,
    .btn-icon-primary-m:disabled,
    .btn-icon-primary-sm:disabled,
    .btn-icon-trash:disabled,
    .btn-icon-trash-m:disabled,
    .btn-icon-trash-sm:disabled {
        opacity: 0.4;
        cursor: not-allowed;
        pointer-events: none;
        box-shadow: none;
        transform: none;
        filter: none;
    }

.tralenso-btn-look-disabled {
    opacity: 0.4;
    cursor: not-allowed;
    box-shadow: none;
}

/* Users who've asked their OS to reduce motion get the colour and shadow changes
   but none of the movement. Scoped to this section's buttons rather than a global
   `* { transition: none }`, which would also kill useful fades. */
@media (prefers-reduced-motion: reduce) {
    .tralenso-primary-btn,
    .tralenso-primary-btn-sm,
    .tralenso-secondary-btn,
    .tralenso-secondary-btn-sm,
    .tralenso-remove-btn,
    .tralenso-remove-btn-sm,
    .btn-icon-primary,
    .btn-icon-primary-m,
    .btn-icon-primary-sm,
    .btn-icon-trash,
    .btn-icon-trash-m,
    .btn-icon-trash-sm {
        transition-duration: 0.01ms;
    }

        .tralenso-primary-btn:hover,
        .tralenso-primary-btn-sm:hover,
        .tralenso-primary-btn:active,
        .tralenso-primary-btn-sm:active,
        .tralenso-secondary-btn:active,
        .tralenso-secondary-btn-sm:active,
        .tralenso-remove-btn:active,
        .tralenso-remove-btn-sm:active,
        .btn-icon-primary:hover,
        .btn-icon-primary-m:hover,
        .btn-icon-primary-sm:hover,
        .btn-icon-trash:hover,
        .btn-icon-trash-m:hover,
        .btn-icon-trash-sm:hover,
        .btn-icon-primary:active,
        .btn-icon-primary-m:active,
        .btn-icon-primary-sm:active,
        .btn-icon-trash:active,
        .btn-icon-trash-m:active,
        .btn-icon-trash-sm:active {
            transform: none;
        }
}

@media (max-width: 1535px) {
    .tralenso-primary-btn,
    .tralenso-secondary-btn,
    .tralenso-remove-btn {
        padding: 0.7rem 1.4rem;
        font-size: 1.4rem;
    }

    .tralenso-primary-btn-sm,
    .tralenso-secondary-btn-sm,
    .tralenso-remove-btn-sm {
        padding: 0.45rem 1rem;
        font-size: 1.15rem;
    }

    .btn-icon-primary,
    .btn-icon-trash {
        padding: 0.5rem;
        border-radius: 0.7rem;
    }

        .btn-icon-primary img,
        .btn-icon-trash img {
            width: 2rem;
            height: 2rem;
        }
}

@media (max-width: 1279px) {
}

@media (max-width: 1023px) {
}

@media (max-width: 767px) {
    .tralenso-primary-btn,
    .tralenso-secondary-btn,
    .tralenso-remove-btn {
        padding: 0.65rem 1.2rem;
        font-size: 1.35rem;
        border-radius: 0.8rem;
    }

    .tralenso-primary-btn-sm,
    .tralenso-secondary-btn-sm,
    .tralenso-remove-btn-sm {
        padding: 0.6rem 0.8rem;
        font-size: 1.2rem;
        border-radius: 0.6rem;
    }

    /* The empty .btn-icon-primary-m {} rule and both !important flags are gone —
       nothing else targets .btn-icon-primary-m img at this width, so there was
       never a conflict for them to win. */
    .btn-icon-primary-m img,
    .btn-icon-trash-m img {
        width: 1.8rem;
        height: 1.8rem;
    }
}

@media (max-width: 575px) {
    .tralenso-primary-btn,
    .tralenso-secondary-btn,
    .tralenso-remove-btn {
        padding: 0.6rem 1.2rem;
        font-size: 1.3rem;
        border-radius: 0.8rem;
    }

    .tralenso-primary-btn-sm,
    .tralenso-secondary-btn-sm,
    .tralenso-remove-btn-sm {
        padding: 0.6rem 0.8rem;
        font-size: 1rem;
        border-radius: 0.7rem;
    }

    .btn-icon-primary,
    .btn-icon-trash {
        padding: 0.4rem;
        border-radius: 0.6rem;
    }

        .btn-icon-primary img,
        .btn-icon-trash img {
            width: 1.6rem;
            height: 1.6rem;
        }
}

/* ============================================
   5. FORM ELEMENTS

   Resting state is quiet (hairline border, no glow); focus is the only thing that
   lights up. Same interaction rules as the buttons: named transition properties,
   pointer-gated hover, nothing that changes layout.
   ============================================ */

/* Input Fields */
.tralenso-input-field {
    /* min-height, not height. A fixed height fights the padding — it silently wins,
       so vertical padding was decorative. min-height lets the field grow if the font
       scales up (it does, at ≤1023px) instead of cropping the text. */
    min-height: 3.5rem;
    border-radius: 0.8rem;
    /* Hairline tint instead of solid #7C7C7C. At full strength every field on a form
       read as a hard box; a tint recedes at rest and makes the orange focus border
       the only strong edge on screen — which is the whole point of a focus state. */
    border: 0.1rem solid rgba(0, 0, 0, 0.14);
    padding: 0.5rem 1rem;
    font-size: 1.4rem;
    font-weight: 400;
    color: var(--color-dark-text);
    width: 100%;
    background: white;
    /* Otherwise the caret is the OS default blue inside an orange-focused field. */
    caret-color: var(--color-primary);
    -webkit-tap-highlight-color: transparent;
    /* Named properties. `all` was animating the box-shadow's spread from 0 on every
       focus, which is what made the ring look like it inflated rather than faded in. */
    transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}

    .tralenso-input-field:focus {
        outline: none;
        border-color: var(--color-primary);
        /* Was rgba(52, 152, 219, 0.1) — Bootstrap BLUE, left over from a template. The
           textarea directly below already used orange, so the same form had two focus
           colours depending on which control you were in. */
        box-shadow: 0 0 0 0.3rem rgba(255, 149, 0, 0.15);
    }

    .tralenso-input-field:disabled {
        background-color: var(--color-bg-secondary);
        color: var(--color-gray-text);
        border-color: rgba(0, 0, 0, 0.08);
        cursor: not-allowed;
        /* opacity dropped. It faded the border and text together on top of colours
           that are already muted, which pushed the label under contrast minimums.
           The greyed background carries "disabled" on its own. */
    }

    .tralenso-input-field::placeholder {
        color: var(--color-gray-text);
        font-weight: 400;
    }

@media (hover: hover) and (pointer: fine) {
    /* Gated: ungated, tapping a field on a phone left the border darkened after blur,
       so a form the user had finished with still looked half-active.
       :not(:focus) as well — without it, hovering a focused field pulled the border
       back down from primary to grey. */
    .tralenso-input-field:hover:not(:disabled):not(:focus) {
        border-color: rgba(0, 0, 0, 0.28);
    }
}

.tralenso-input-field-title {
    font-size: 1.2rem;
    color: var(--color-dark-gray-text);
    font-weight: 500;
}

.tralenso-input-field-left-title {
    color: var(--color-gray-text);
    font-size: 1.6rem;
    font-weight: 400;
}

.tralenso-input-field--readonly {
    background-color: var(--color-bg-secondary);
    color: var(--color-dark-gray-text);
    border-color: rgba(0, 0, 0, 0.08);
    cursor: not-allowed;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Textarea */
.tralenso__textarea {
    border-radius: 0.8rem;
    border: 0.1rem solid rgba(0, 0, 0, 0.14);
    padding: 1rem 1.5rem;
    font-size: 1.5rem;
    font-weight: 400;
    color: var(--color-dark-text);
    width: 100%;
    background: white;
    resize: vertical;
    min-height: 12rem;
    caret-color: var(--color-primary);
    -webkit-tap-highlight-color: transparent;
    transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}

    .tralenso__textarea:focus {
        outline: none;
        border-color: var(--color-primary);
        box-shadow: 0 0 0 0.3rem rgba(255, 149, 0, 0.15);
    }

    .tralenso__textarea:disabled {
        background-color: var(--color-bg-secondary);
        color: var(--color-gray-text);
        border-color: rgba(0, 0, 0, 0.08);
        cursor: not-allowed;
    }

    .tralenso__textarea::placeholder {
        color: var(--color-gray-text);
        font-weight: 400;
    }

@media (hover: hover) and (pointer: fine) {
    .tralenso__textarea:hover:not(:disabled):not(:focus) {
        border-color: rgba(0, 0, 0, 0.28);
    }
}

/* Validation States */
.tralenso-input-field.invalid,
.tralenso__textarea.invalid {
    border-color: var(--color-danger) !important;
}

    /* The border went red but the focus GLOW stayed orange, so a field the user was
       actively fixing was ringed in the brand's "all good" colour. The ring has to
       agree with the border. */
    .tralenso-input-field.invalid:focus,
    .tralenso__textarea.invalid:focus {
        box-shadow: 0 0 0 0.3rem rgba(239, 68, 68, 0.15);
    }

.tralenso-input-wrapper {
    position: relative;
}

.tralenso-validation-error {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 1.2rem;
    color: var(--color-danger);
    margin-top: 0.3rem;
    overflow-wrap: break-word;
    word-break: break-word;
    min-width: 0;
}

    .tralenso-validation-error::before {
        content: "\F431"; /* Bootstrap Icons info-circle */
        font-family: "bootstrap-icons";
        font-size: 1.2rem;
        /* The icon is decoration — the text beside it already says what's wrong.
           Without this, screen readers announce the private-use glyph as garbage. */
        speak: none;
    }

/* Input Status Icons */
.tralenso-input-status {
    position: absolute;
    top: 50%;
    right: 1.5rem;
    transform: translateY(-50%);
    font-size: 1.6rem;
    pointer-events: none;
}

.tralenso-input-status--success {
    color: var(--color-success);
}

input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

input[type=number] {
    -moz-appearance: textfield;
    appearance: textfield;
}

@media (prefers-reduced-motion: reduce) {
    .tralenso-input-field,
    .tralenso__textarea {
        transition-duration: 0.01ms;
    }
}

@media (max-width: 1535px) {
    .tralenso__textarea {
        font-size: 1.4rem;
    }

    .tralenso-input-field {
        min-height: 3.2rem;
        font-size: 1.3rem;
    }

    .tralenso-input-field-left-title {
        font-size: 1.4rem;
    }
}

@media (max-width: 1279px) {
}

@media (max-width: 1023px) {
    /* 1.6rem = 16px, and 16px is the threshold below which iOS Safari zooms the page
       on focus. That was already right — but the field was still only 35px tall, so
       it cleared the zoom rule and failed the 44pt touch minimum at the same time.
       min-height fixes the half that was missing. */
    .tralenso__textarea {
        font-size: 1.6rem;
    }

    .tralenso-input-field {
        font-size: 1.6rem;
        min-height: 4.4rem;
    }

    /* The status tick is centred on the field's midpoint, so a taller field needs no
       change — but the padding does, or the text sits high in the new height. */
    .tralenso-input-field {
        padding: 0.8rem 1.2rem;
    }
}

@media (max-width: 767px) {
}

@media (max-width: 575px) {
    .tralenso-input-field-left-title {
        color: var(--color-gray-text);
        font-size: 1.3rem;
        font-weight: 400;
    }
}


/* ============================================
   6. CHARACTER COUNTER
   ============================================ */

.tralenso-char-counter {
    font-size: 1.2rem;
    color: var(--color-gray-text);
    margin-left: auto;
    /* Fixed-width digits. With proportional figures a "1" is narrower than a "0", so
       the counter's width changed on almost every keystroke and the whole number
       shuffled sideways as you typed. */
    font-variant-numeric: tabular-nums;
    transition: color 0.2s ease;
}

/* Was the #f59e0b literal — which is exactly --color-warning, so this was a copy of
   the token sitting next to the token. */
.tralenso-char-counter--warning {
    color: var(--color-warning);
}

.tralenso-char-counter--limit {
    color: var(--color-danger);
    font-weight: 500;
}

.tralenso-char-counter--hidden {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    .tralenso-char-counter {
        transition-duration: 0.01ms;
    }
}


/* ============================================
   7. FILE UPLOAD
   ============================================ */

.tralenso-upload-hidden {
    display: none;
}

.tralenso-upload-label {
    display: inline-block;
    background: var(--gradient-primary);
    color: #fff;
    padding: 0.4rem 1rem;
    border-radius: 0.6rem;
    cursor: pointer;
    font-size: 1.2rem;
    font-weight: 500;
    text-align: center;
    width: fit-content;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    box-shadow: 0 0.15rem 0.4rem rgba(255, 149, 0, 0.22);
    /* `cursor: pointer` was declared twice in this rule — removed the duplicate.
       filter over a background swap, same fix as the buttons: the old hover replaced
       the gradient with flat --color-primary-dark, and `background` wasn't in the
       transition list, so it snapped AND lost the gradient. */
    transition: filter 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}

    .tralenso-upload-label:active {
        transform: scale(0.96);
        box-shadow: 0 0.1rem 0.2rem rgba(255, 149, 0, 0.18);
    }

@media (hover: hover) and (pointer: fine) {
    .tralenso-upload-label:hover {
        filter: brightness(1.08);
        color: #fff;
        transform: translateY(-0.1rem);
        box-shadow: 0 0.3rem 0.8rem rgba(255, 149, 0, 0.28);
    }
}

.tralenso-upload-wrapper {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

    /* The real <input type="file"> is display:none, so it can never show a focus ring
       of its own — a keyboard user tabbing to this control previously got NO visible
       indication of where they were. :focus-within on the wrapper puts the ring on
       the visible label instead. */
    .tralenso-upload-wrapper:focus-within .tralenso-upload-label {
        outline: 0.2rem solid var(--color-primary);
        outline-offset: 0.2rem;
    }

.file-name {
    /* Was 0.85rem — which at the 62.5% root is 8.5px, less than half the size of the
       smallest text anywhere else in this file. Almost certainly a rem/em slip that
       nobody caught because the filename is usually short enough to still be legible
       as a grey smudge. 1.2rem matches .tralenso-input-field-title beside it. */
    font-size: 1.2rem;
    color: var(--color-gray-text);
    /* Long filenames pushed the form wider instead of truncating. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 100%;
}

@media (prefers-reduced-motion: reduce) {
    .tralenso-upload-label {
        transition-duration: 0.01ms;
    }

        .tralenso-upload-label:hover,
        .tralenso-upload-label:active {
            transform: none;
        }
}


/* ============================================
   8. CHECKBOXES
   ============================================ */

.tralenso-checkbox-container {
    display: flex;
    align-items: center;
    gap: 0.8rem;
}

.tralenso-checkbox-label {
    /* Only meaningful when the markup uses <label for="...">. Where it does, the whole
       label becomes tap area — which is the difference between a 19px target and a
       comfortable one, without changing the box's size. */
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.tralenso-default-checkbox {
    appearance: none;
    -webkit-appearance: none;
    /* rem, not em. At 1.2em the box silently resized with whatever font-size its
       parent happened to have, so the "same" checkbox was a different size on a
       modal, a form row and a card. */
    width: 1.9rem;
    height: 1.9rem;
    /* Without this, a long label in a flex row squeezes the box into an oval. */
    flex-shrink: 0;
    border: 0.1rem solid rgba(0, 0, 0, 0.22);
    /* 0.5rem, up from 0.2em (~3px). The old value was nearly a hard square, which
       was the last right-angle left in a UI that's otherwise 0.8–1rem rounded. */
    border-radius: 0.5rem;
    background: white;
    cursor: pointer;
    position: relative;
    display: inline-block;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, transform 0.12s ease;
}

    /* The tick, DRAWN rather than typed.
       It used to be the text character "✓", which meant its shape, weight and size
       came from whatever font resolved on that platform — a different glyph on
       Windows, macOS, iOS and Android. Worse, the old ::after was position: absolute
       with no offsets and relied on `justify-self: center`, which does nothing to an
       absolutely positioned element. It was centred by luck.
       Two borders on a rotated box give an identical tick everywhere, at any size. */
    .tralenso-default-checkbox::after {
        content: "";
        position: absolute;
        left: 50%;
        top: 50%;
        width: 0.45rem;
        height: 0.9rem;
        border: solid white;
        border-width: 0 0.2rem 0.2rem 0;
        /* -55% rather than -50%: a checkmark's optical centre sits slightly above its
           bounding box's centre, because the long stroke runs down-right. */
        transform: translate(-50%, -55%) rotate(45deg) scale(0);
        transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
    }

    .tralenso-default-checkbox:checked {
        background: var(--color-primary);
        border-color: var(--color-primary);
    }

        /* Slight overshoot on the way in (the cubic-bezier above is a back-out curve),
           so ticking feels like a physical snap rather than an instant swap. */
        .tralenso-default-checkbox:checked::after {
            transform: translate(-50%, -55%) rotate(45deg) scale(1);
        }

    .tralenso-default-checkbox:active:not(:disabled) {
        transform: scale(0.9);
    }

    .tralenso-default-checkbox:focus-visible {
        outline: none;
        box-shadow: 0 0 0 0.3rem rgba(255, 149, 0, 0.25);
    }

    .tralenso-default-checkbox:disabled {
        background: var(--color-bg-secondary);
        border-color: rgba(0, 0, 0, 0.1);
        cursor: not-allowed;
    }

        .tralenso-default-checkbox:disabled:checked {
            background: var(--color-gray-text);
            border-color: var(--color-gray-text);
        }

@media (hover: hover) and (pointer: fine) {
    .tralenso-default-checkbox:hover:not(:disabled):not(:checked) {
        border-color: var(--color-primary);
        background: var(--color-primary-extra-light);
    }

    .tralenso-default-checkbox:hover:not(:disabled):checked {
        filter: brightness(1.08);
    }
}

@media (prefers-reduced-motion: reduce) {
    .tralenso-default-checkbox,
    .tralenso-default-checkbox::after {
        transition-duration: 0.01ms;
    }

        .tralenso-default-checkbox:active:not(:disabled) {
            transform: none;
        }
}

@media (max-width: 1535px) {
}

@media (max-width: 1279px) {
    .tralenso-checkbox-label {
        font-size: 1.5rem;
    }
}

@media (max-width: 1023px) {
}

@media (max-width: 767px) {
    .tralenso-checkbox-label {
        font-size: 1.4rem;
    }

    /* Deliberately NOT scaled down with the label. On touch the box is the target,
       and every step smaller costs real tap accuracy — the type can shrink, the
       control can't. */
    .tralenso-default-checkbox {
        width: 2rem;
        height: 2rem;
    }
}

@media (max-width: 575px) {
    .tralenso-checkbox-label {
        font-size: 1.3rem;
    }
}

/* ============================================
   9. CATEGORIES & ICONS
   ============================================ */

.traLenso-category-icon {
    width: 4rem;
    height: 4rem;
    /* currentColor, not `red`. The mask means this is the icon's ink colour, and it's
       overridden by the .category-btn rules below — but a category rendered outside
       those (or with a --icon-url that fails to resolve) drew a solid RED SQUARE.
       currentColor degrades to the surrounding text colour instead. */
    background-color: currentColor;
    color: var(--color-gray-text);
    mask-image: var(--icon-url);
    mask-size: contain;
    mask-repeat: no-repeat;
    mask-position: center;
    -webkit-mask-image: var(--icon-url);
    -webkit-mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    -webkit-mask-position: center;
    transition: background-color 0.2s ease;
}

.category-btn .traLenso-category-icon {
    background-color: var(--color-gray-text);
}

.category-btn.active .traLenso-category-icon {
    background-color: var(--color-dark-text);
}

@media (hover: hover) and (pointer: fine) {
    /* Gated — ungated, tapping a category on a phone left its icon darkened, so an
       unselected category looked selected until you tapped something else. */
    .category-btn:hover .traLenso-category-icon {
        background-color: var(--color-dark-text);
    }
}

/* Circle Icon with colored background */
.category-icon-circle {
    width: 5rem;
    height: 5rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    /* Was `solid 0.3rem #e9e9e9` — a 3px hard ring, the heaviest border left in this
       file, in a literal that is exactly --color-light-gray. A translucent hairline
       plus a soft lift separates the circle from the page without drawing a band
       around it, and composites correctly on a card, a photo or the background. */
    border: 0.1rem solid rgba(255, 255, 255, 0.9);
    box-shadow: 0 0.1rem 0.4rem rgba(0, 0, 0, 0.12);
    flex-shrink: 0;
}

    .category-icon-circle img,
    .category-icon-circle svg {
        width: 65%;
        height: 65%;
        filter: brightness(0) invert(1); /* Makes the icon white */
    }

        /* ── Per-icon optical nudges ────────────────────────────────────────────────────
   Individual SVGs have uneven internal padding, so a few need a pixel or two to sit
   optically centred. Every selector here MUST carry its own .category-icon-circle
   prefix.

   ⚠️ The previous version wrote them as a comma list with the class on its own line:

       .category-icon-circle
       img[src*="nature-explorer"],
       img[src*="food-enthusiast"],
       ...

   The line break is only whitespace, so that parsed as ONE scoped descendant
   selector followed by THREE GLOBAL ones — `img[src*="food-enthusiast"]`,
   `img[src*="urban-explorer"]` and `img[src*="coastal-chaser"]` were matching every
   image on the site with those strings in its URL: filter chips, marker icons, the
   guide-create picker, activity cards. Each got a stray 2px bottom margin, which is
   why those three categories sat slightly low everywhere relative to the other 25.
   Duplicated verbatim in the --sm block below, so it fired twice. */

        .category-icon-circle img[src*="vegan-friendly"] {
            margin-bottom: -0.1rem;
        }

        .category-icon-circle img[src*="nature-explorer"],
        .category-icon-circle img[src*="food-enthusiast"],
        .category-icon-circle img[src*="urban-explorer"],
        .category-icon-circle img[src*="coastal-chaser"] {
            margin-bottom: 0.2rem;
        }

        .category-icon-circle img[src*="budget-traveler"] {
            margin-left: 0.2rem;
        }

        .category-icon-circle img[src*="trail-seeker"] {
            margin-left: 0.3rem;
        }

/* Small variant for ActivityCards */
.category-icon-circle--sm {
    width: 3.4rem;
    height: 3.4rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    border: none;
    flex-shrink: 0;
}

    .category-icon-circle--sm img,
    .category-icon-circle--sm svg {
        width: 65%;
        height: 65%;
        filter: brightness(0) invert(1); /* Makes the icon white */
    }

        /* Same nudges, same rule: the class prefix repeats on every line. */
        .category-icon-circle--sm img[src*="vegan-friendly"] {
            margin-bottom: -0.1rem;
        }

        .category-icon-circle--sm img[src*="nature-explorer"],
        .category-icon-circle--sm img[src*="food-enthusiast"],
        .category-icon-circle--sm img[src*="urban-explorer"],
        .category-icon-circle--sm img[src*="coastal-chaser"] {
            margin-bottom: 0.2rem;
        }

        .category-icon-circle--sm img[src*="budget-traveler"] {
            margin-left: 0.2rem;
        }

        .category-icon-circle--sm img[src*="trail-seeker"] {
            margin-left: 0.3rem;
        }

/* Category Icon Responsive Scaling */
@media (max-width: 1535px) {
    .category-icon-circle {
        width: 4.6rem;
        height: 4.6rem;
    }

    .category-icon-circle--sm {
        width: 3.2rem;
        height: 3.2rem;
    }
}

@media (max-width: 1023px) {
    .category-icon-circle {
        width: 4.2rem;
        height: 4.2rem;
    }

    .category-icon-circle--sm {
        width: 3.2rem;
        height: 3.2rem;
    }
}

@media (max-width: 575px) {
    .category-icon-circle {
        width: 3.8rem;
        height: 3.8rem;
    }

    .category-icon-circle--sm {
        width: 2.8rem;
        height: 2.8rem;
    }
}

/* ============================================
   10. SMART FILTERS
   ============================================ */

.smart-filter-icon {
    width: 3.8rem;
    height: 3.8rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    /* Shared by all three tiers so a new tier can't be added without it. */
    transition: transform 0.15s ease, box-shadow 0.2s ease;
}

    .smart-filter-icon img {
        padding-left: 0.1rem;
        padding-bottom: 0.1rem;
        width: 2.2rem;
        height: 2.2rem;
        filter: brightness(0) invert(1);
    }

    /* Shadows converted from px to rem — the only px values left in this file, and
       they don't scale with the 62.5% root the rest of the system is built on.
       Tightened to 0.2rem/0.6rem and dropped to 0.35 alpha: at 8px/0.4 the medal
       glow was heavier than the shadow under a real elevated card. */
    .smart-filter-icon.gold {
        background: linear-gradient(135deg, #FFD700, #dd8f00);
        box-shadow: 0 0.2rem 0.6rem rgba(255, 215, 0, 0.35);
    }

    .smart-filter-icon.silver {
        background: linear-gradient(135deg, #C0C0C0, #8d8d8d);
        box-shadow: 0 0.2rem 0.6rem rgba(192, 192, 192, 0.35);
    }

    .smart-filter-icon.bronze {
        background: linear-gradient(135deg, #CD7F32, #9b622d);
        box-shadow: 0 0.2rem 0.6rem rgba(205, 127, 50, 0.35);
    }

.smart-filter-title {
    font-weight: 600;
}

.smart-filter-count {
    font-size: 1.6rem;
    color: var(--color-gray-text);
    font-weight: 400;
    /* Counts change as filters apply; proportional digits made the number shuffle
       sideways on each update. */
    font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
    .smart-filter-icon,
    .traLenso-category-icon {
        transition-duration: 0.01ms;
    }
}

@media (max-width: 1023px) {
    .smart-filter-icon {
        width: 3.4rem;
        height: 3.4rem;
    }

        .smart-filter-icon img {
            width: 2rem;
            height: 2rem;
        }
}

@media (max-width: 767px) {
}

@media (max-width: 575px) {
    .smart-filter-icon {
        width: 3rem;
        height: 3rem;
    }

        .smart-filter-icon img {
            padding-left: 0rem;
            width: 1.8rem;
            height: 1.8rem;
        }
}

/* ============================================
   11. REVIEWS & RATINGS
   ============================================ */

.star-rating {
    display: flex;
    align-items: center;
    font-size: 2rem;
    gap: 0.3rem;
    /* Stars are a rendered value, not a text selection target — dragging across a
       rating row shouldn't highlight five glyphs. */
    user-select: none;
}

.star-gradient {
    background: var(--gradient-star);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

.star-rating-value {
    /* rem, not em. At 1.1em this compounded off whatever .star-rating's font-size
       resolved to in that context — 22px next to desktop stars, 16.5px next to the
       mobile ones — so the number was a different size on every screen for no stated
       reason. A fixed size scales explicitly in the media queries below. */
    font-size: 1.7rem;
    font-weight: 700;
    /* Was #fbbf24, which is the top stop of --gradient-star written out longhand. */
    color: #fbbf24;
    color: color-mix(in srgb, var(--color-warning) 85%, white);
    margin-right: 0.3rem;
    /* The rating changes as reviews come in; proportional digits made "4.1" and "4.0"
       different widths, so the stars beside it shifted. */
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

@media (max-width: 1023px) {
    .star-rating {
        font-size: 1.7rem;
    }

    .star-rating-value {
        font-size: 1.5rem;
    }
}

@media (max-width: 575px) {
    .star-rating {
        font-size: 1.5rem;
    }

    .star-rating-value {
        font-size: 1.35rem;
    }
}
/* ============================================
   12. MODALS
   ============================================ */

.tralenso-close-modal-icon {
    position: absolute;
    top: 0;
    /* Kept OUTSIDE the modal on wide screens — that's the existing layout and moving
       it inboard would overlap content in modals that were built around this offset.
       The ≤767px block below pulls it inside, where -3rem would either be clipped by
       an overflow:hidden ancestor or hang off the viewport edge. */
    right: -3rem;
    z-index: 10;
    /* The button now centres its own cross. It previously relied on the child's
       `place-self: center`, which does nothing unless the parent is flex or grid —
       a <button> is neither, so the cross was centred by luck. */
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--gradient-red);
    border: none;
    color: white;
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    cursor: pointer;
    /* 0.8, not 0.65. The old value was faint enough to look disabled, and since hover
       ALSO set 0.65 there was nothing left for the hover to change. */
    opacity: 0.8;
    -webkit-tap-highlight-color: transparent;
    transition: opacity 0.2s ease, filter 0.2s ease, transform 0.15s ease;
}

    .tralenso-close-modal-icon:active {
        transform: scale(0.9);
    }

    .tralenso-close-modal-icon:focus-visible {
        outline: 0.2rem solid white;
        outline-offset: 0.2rem;
    }

@media (hover: hover) and (pointer: fine) {
    /* Opacity + brightness, not a background swap. The old rule replaced
       --gradient-red with flat --color-dark-red, which killed the gradient AND
       couldn't animate — `background` wasn't in the transition list. */
    .tralenso-close-modal-icon:hover {
        opacity: 1;
        filter: brightness(1.08);
        transform: scale(1.06);
    }
}

.tralenso-close-modal-icon-cross {
    display: block;
    height: 1.4rem;
    width: 1.4rem;
    /* place-self removed — the parent does the centring now. */
}

@media (prefers-reduced-motion: reduce) {
    .tralenso-close-modal-icon {
        transition-duration: 0.01ms;
    }

        .tralenso-close-modal-icon:hover,
        .tralenso-close-modal-icon:active {
            transform: none;
        }
}

@media (max-width: 767px) {
    /* Inside the modal, top-right. At -3rem the button sat 30px past the modal's right
       edge — on a 375px phone where the modal already fills nearly the whole width,
       that's off-screen or clipped. The ring separates it from whatever content it
       now sits on top of. */
    .tralenso-close-modal-icon {
        top: 0.8rem;
        right: 0.8rem;
        width: 3.2rem;
        height: 3.2rem;
        opacity: 1;
        box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.85);
    }
}


/* ============================================
   13. UI STATES (Empty, Loading, Badges)
   ============================================ */

/* Empty State */
.tralenso-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 4rem 2rem;
    text-align: center;
}

.tralenso-empty-state--card {
    background: white;
    border-radius: 1.2rem;
    /* Hairline + a soft lift instead of the solid #dddddd rule. A 1px grey outline on
       a white box against a #FAFAFA page reads as a wireframe placeholder; the shadow
       is what makes it read as a card sitting ON the page. */
    border: 0.1rem solid rgba(0, 0, 0, 0.06);
    box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.04), 0 0.4rem 1.2rem rgba(0, 0, 0, 0.04);
}

/* The glyph gets a soft circular backdrop rather than floating loose in the middle
   of the card. Gives the state a focal point at a glance, which is what makes an
   empty screen feel designed rather than unfinished. */
.tralenso-empty-state__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 7.2rem;
    height: 7.2rem;
    border-radius: 50%;
    background: var(--color-bg-secondary);
    font-size: 3.2rem;
    color: var(--color-gray-text);
    margin-bottom: 1.5rem;
}

.tralenso-empty-state__title {
    font-size: 1.8rem;
    font-weight: 600;
    color: var(--color-dark-text);
    margin-bottom: 0.5rem;
}

.tralenso-empty-state__description {
    font-size: 1.4rem;
    color: var(--color-gray-text);
    /* Long empty-state copy stretched the full width of a wide card, which is hard to
       scan. A measure cap keeps it to a comfortable line length and centres it. */
    max-width: 42rem;
    margin-left: auto;
    margin-right: auto;
}

/* Loading State */
.tralenso-loading-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 4rem 2rem;
    color: var(--color-gray-text);
}

    .tralenso-loading-state p {
        margin-top: 1rem;
        font-size: 1.4rem;
    }

/* Status Badges
   Colours are now derived from the semantic tokens with color-mix instead of eight
   unrelated hex literals — change --color-success once and the badge follows. Same
   fallback-first pattern as .tab-navigation-wrapper: the flat hex is declared first
   for engines without color-mix (older iOS WKWebView), then overridden. */
.tralenso-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.4rem 1rem;
    /* 999px, not 2rem. At 2rem a badge taller than 40px would stop being a pill and
       start being a rounded rectangle — 999px is unconditional. */
    border-radius: 999px;
    font-size: 1.2rem;
    font-weight: 500;
    /* Height was following the inherited line-height (1.6 from body), so the same
       badge was a different height inside a table row than inside a card. */
    line-height: 1;
    padding-block: 0.5rem;
    white-space: nowrap;
}

.tralenso-badge--success {
    background: #d1fae5;
    background: color-mix(in srgb, var(--color-success) 15%, white);
    color: #065f46;
    color: color-mix(in srgb, var(--color-success) 75%, black);
}

.tralenso-badge--warning {
    background: #fef3c7;
    background: color-mix(in srgb, var(--color-warning) 18%, white);
    color: #92400e;
    color: color-mix(in srgb, var(--color-warning) 70%, black);
}

.tralenso-badge--danger {
    background: #fee2e2;
    background: color-mix(in srgb, var(--color-danger) 15%, white);
    color: #991b1b;
    color: color-mix(in srgb, var(--color-danger) 75%, black);
}

.tralenso-badge--info {
    background: #e0f2fe;
    background: color-mix(in srgb, var(--color-info) 15%, white);
    color: #075985;
    color: color-mix(in srgb, var(--color-info) 75%, black);
}

.tralenso-badge--neutral {
    background: var(--color-bg-secondary);
    color: #4b5563;
    color: var(--color-text-secondary);
}

@media (max-width: 1535px) {
    .tralenso-badge {
        font-size: 1.1rem;
    }
}

@media (max-width: 1023px) {
    .tralenso-empty-state__icon {
        width: 5.6rem;
        height: 5.6rem;
        font-size: 2.4rem;
        margin-bottom: 0.8rem;
    }

    .tralenso-empty-state__title {
        font-size: 1.5rem;
    }

    .tralenso-empty-state__description {
        font-size: 1.2rem;
    }
}

@media (max-width: 575px) {
    .tralenso-badge {
        font-size: 1.1rem;
    }

    .tralenso-empty-state {
        padding: 3rem 1.6rem;
    }
}

/* ============================================
   14. TEXT UTILITIES
   ============================================ */

.tralenso-truncate {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}


/* ============================================
   15. DECORATIVE / BORDERS
   ============================================ */

.tralenso-border {
    border-bottom: solid 0.1rem var(--border-gray);
    margin: 0.5rem 0;
}

/* ============================================
   16. FAVORITE HEART BUTTON

   The most-tapped control on the platform, so it gets the fullest treatment:
   a real press, a pop on favourite, and a hover that can't stick on touch.
   ============================================ */
.favorite-heart-btn {
    position: absolute;
    top: 0.8rem;
    right: 0.8rem;
    width: 3.6rem;
    height: 3.6rem;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Glass, same recipe as the nav bars. The heart sits on a photo, and a flat grey
       disc reads as a sticker pasted on top of it; a blurred disc reads as part of
       the image's surface. */
    background: rgba(255, 255, 255, 0.75);
    -webkit-backdrop-filter: blur(8px) saturate(160%);
    backdrop-filter: blur(8px) saturate(160%);
    border: none;
    border-radius: 50%;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    /* Separates the disc from a light photo — without it a white heart on a white sky
       disappears entirely. */
    box-shadow: 0 0.1rem 0.4rem rgba(0, 0, 0, 0.12);
    /* Named properties. `all` was also transitioning width/height/box-shadow spread,
       which is why the button appeared to inflate rather than simply scale. */
    transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
    z-index: 6;
}

/* Opaque where blur isn't supported — a 75% white disc with a photo bleeding
   through is worse than a flat one. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .favorite-heart-btn {
        background: rgba(255, 255, 255, 0.92);
    }
}

.favorite-heart-btn i {
    font-size: 1.7rem;
    /* Was #9ca3af — a hardcoded grey with no relationship to the palette. */
    color: var(--color-gray-text);
    line-height: 1;
    transition: color 0.2s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Press feedback the old rule had none of. Favouriting had no confirmation on
       touch at all — the icon just changed colour, which is easy to miss mid-scroll. */
.favorite-heart-btn:active {
    transform: scale(0.88);
}

.favorite-heart-btn:focus-visible {
    outline: 0.2rem solid var(--color-primary);
    outline-offset: 0.2rem;
}

/* Was #ef4444 — which IS --color-danger, so this was the token written out longhand. */
.favorite-heart-btn.is-favorited i {
    color: var(--color-danger);
}

/* The pop. Back-out curve on the icon's transform, triggered by the class flipping
       on — so the heart overshoots slightly and settles. This is the confirmation
       that the tap landed, and it works identically on touch and pointer. */
.favorite-heart-btn.is-favorited {
    background: rgba(255, 255, 255, 0.85);
}

    .favorite-heart-btn.is-favorited i {
        transform: scale(1.12);
    }

    /* Pressing a favourited heart squashes the icon too, so un-favouriting feels as
       physical as favouriting. */
    .favorite-heart-btn.is-favorited:active i {
        transform: scale(0.95);
    }

@media (hover: hover) and (pointer: fine) {
    /* Ungated, a tap left the button parked at 1.1× with a pink icon — on a card that
       was NOT favourited that looks exactly like the favourited state, so the user
       couldn't tell whether their tap had worked. */
    .favorite-heart-btn:hover {
        background: rgba(255, 255, 255, 0.95);
        box-shadow: 0 0.2rem 0.8rem rgba(0, 0, 0, 0.16);
        transform: scale(1.1);
    }

        /* color-mix instead of the #f87171 literal — a lighter --color-danger, so the
           preview tint and the committed colour can never drift apart. */
        .favorite-heart-btn:hover i {
            color: color-mix(in srgb, var(--color-danger) 70%, white);
        }

    /* Was #dc2626, another loose literal. */
    .favorite-heart-btn.is-favorited:hover i {
        color: color-mix(in srgb, var(--color-danger) 85%, black);
    }
}

@media (prefers-reduced-motion: reduce) {
    .favorite-heart-btn,
    .favorite-heart-btn i {
        transition-duration: 0.01ms;
    }

        .favorite-heart-btn:hover,
        .favorite-heart-btn:active {
            transform: none;
        }

        /* Colour still carries the state; only the pop is dropped. */
        .favorite-heart-btn.is-favorited i,
        .favorite-heart-btn.is-favorited:active i {
            transform: none;
        }
}

/* Inline variant (for cards without images, e.g. TipCard, EventCard).
   No photo behind it, so the glass and the shadow are both pointless here — they'd
   just draw a faint disc on a white card. */
.favorite-heart-btn--inline {
    position: static;
    margin-left: auto;
    flex-shrink: 0;
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
}

    .favorite-heart-btn--inline.is-favorited {
        background: transparent;
    }

@media (hover: hover) and (pointer: fine) {
    .favorite-heart-btn--inline:hover {
        background: rgba(0, 0, 0, 0.05);
        box-shadow: none;
    }
}

/* Touch targets. 3.6rem is 36px — still under the 44pt minimum, and this button sits
   in the corner of a card that is itself a link, so a near-miss NAVIGATES instead of
   favouriting. The pseudo-element extends the hit area without changing the visible
   disc or disturbing the card's layout. */
.favorite-heart-btn::before {
    content: "";
    position: absolute;
    inset: -0.4rem;
    border-radius: 50%;
}

@media (max-width: 767px) {
    .favorite-heart-btn {
        width: 3.8rem;
        height: 3.8rem;
    }

        .favorite-heart-btn i {
            font-size: 1.8rem;
        }

        /* Wider on touch, where accuracy is worst — 3.8 + 2×0.6 = 5rem of real target. */
        .favorite-heart-btn::before {
            inset: -0.6rem;
        }
}

/* ============================================
   17. TABLES
   ============================================ */

.tralenso-table-container {
    background: white;
    border-radius: 1.2rem;
    /* Hairline + soft lift, matching .tralenso-empty-state--card. The solid #dddddd
       outline made the table read as a wireframe rather than a surface. */
    border: 0.1rem solid rgba(0, 0, 0, 0.06);
    box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.04), 0 0.4rem 1.2rem rgba(0, 0, 0, 0.04);
    /* overflow: hidden clipped wide tables on narrow screens — columns simply vanished
       with nothing to say they existed. overflow-x: auto scrolls instead, and hidden
       on the Y axis still keeps the corner radius clipping the first/last rows.
       -webkit-overflow-scrolling for momentum in the MAUI WKWebView. */
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    /* Snapping the scroll to column edges stops a table resting mid-cell. */
    scrollbar-width: thin;
}

.tralenso-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 1.4rem;
}

    .tralenso-table thead {
        /* Was #f8f9fa — a literal that happens to be near --color-bg-secondary. */
        background: var(--color-bg-secondary);
    }

    .tralenso-table th {
        padding: 1.2rem 1.5rem;
        font-weight: 600;
        color: var(--color-dark-gray-text);
        font-size: 1.2rem;
        text-transform: uppercase;
        /* text-align, NOT text-align-last. The latter only aligns the final line of a
           block — it worked here purely because every cell happened to be one line,
           and any wrapping header silently left its first lines left-aligned. */
        text-align: center;
        letter-spacing: 0.05rem;
        white-space: nowrap;
        /* Column labels survive a long scroll. Needs the container to be the scroll
           parent, which it is. */
        position: sticky;
        top: 0;
        z-index: 1;
        background: var(--color-bg-secondary);
        border-bottom: 0.1rem solid rgba(0, 0, 0, 0.08);
    }

        .tralenso-table th:first-child {
            text-align: left;
        }

    .tralenso-table td {
        padding: 1.2rem 1.5rem;
        border-bottom: 0.1rem solid rgba(0, 0, 0, 0.06);
        color: var(--color-dark-text);
        text-align: center;
    }

        .tralenso-table td:first-child {
            text-align: left;
        }

        /* Money and counts line up in a column when the digits are fixed-width. Opt-in per
       cell rather than global, since it hurts proportional text. */
        .tralenso-table td.is-numeric,
        .tralenso-table th.is-numeric {
            font-variant-numeric: tabular-nums;
            text-align: right;
        }

    .tralenso-table tbody tr:last-child td {
        border-bottom: none;
    }

/* Row hover moved under .tralenso-table__row--clickable only. The old blanket
   `tbody tr:hover` tinted EVERY row on every table, so static rows looked clickable
   and nothing happened when you clicked them. A hover cue should mean something. */
.tralenso-table__row--clickable {
    cursor: pointer;
    transition: background 0.15s ease;
    -webkit-tap-highlight-color: transparent;
}

    .tralenso-table__row--clickable.expanded {
        background: var(--color-primary-extra-light);
    }

    .tralenso-table__row--clickable:active {
        background: var(--color-primary-light);
    }

@media (hover: hover) and (pointer: fine) {
    .tralenso-table__row--clickable:hover {
        background: var(--color-primary-extra-light);
    }
}

@media (prefers-reduced-motion: reduce) {
    .tralenso-table__row--clickable {
        transition-duration: 0.01ms;
    }
}

@media (max-width: 767px) {
    .tralenso-table th,
    .tralenso-table td {
        padding: 1rem;
    }

    /* The container scrolls now, so cells must not compress themselves to fit — that's
       what turns a readable row into three lines of two-character wraps. */
    .tralenso-table td {
        white-space: nowrap;
    }
}

@media (max-width: 575px) {
    .tralenso-table th,
    .tralenso-table td {
        padding: 0.8rem 0.6rem;
        font-size: 1.2rem;
    }
}

/* ============================================
   18. ANALYTICS TIME RANGE SELECTOR
   ============================================ */
.analytics-time-selector {
    display: flex;
    gap: 0.5rem;
    background: var(--color-bg-secondary);
    border-radius: 1rem;
    width: fit-content;
    /* The track had no padding, so the active pill's edge sat flush against the
       container's — which read as a mis-drawn border rather than a pill in a groove. */
    padding: 0.4rem;
}

.analytics-time-btn {
    padding: 0.6rem 1.2rem;
    border: none;
    border-radius: 0.75rem;
    background: transparent;
    color: var(--color-text-secondary);
    font-size: 1.3rem;
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}

    .analytics-time-btn:active:not(:disabled) {
        transform: scale(0.95);
    }

    .analytics-time-btn:focus-visible {
        outline: 0.2rem solid var(--color-primary);
        outline-offset: 0.2rem;
    }

    .analytics-time-btn.is-active {
        background: var(--gradient-primary);
        color: white;
        /* `transition: none` removed. It made the selected range snap in while every
           other state in the same control faded — the one interaction the user
           actually performs was the only one with no motion. */
        box-shadow: 0 0.1rem 0.3rem rgba(255, 149, 0, 0.3);
    }

    .analytics-time-btn:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }

@media (hover: hover) and (pointer: fine) {
    .analytics-time-btn:hover:not(:disabled):not(.is-active) {
        background: rgba(0, 0, 0, 0.04);
        color: var(--color-dark-text);
    }

    .analytics-time-btn.is-active:hover:not(:disabled) {
        filter: brightness(1.06);
    }
}

@media (prefers-reduced-motion: reduce) {
    .analytics-time-btn {
        transition-duration: 0.01ms;
    }

        .analytics-time-btn:active:not(:disabled) {
            transform: none;
        }
}

/* ============================================
   19. BLOCKING OVERLAY
   Full-viewport blocker during a slow in-flight action (checkout
   redirect, publish, Stripe Connect). z-index sits above page content
   + the upsell sidebar (1050) but below the toast layer (1055), so a
   failure toast stays visible. Used via the <BlockingOverlay /> component.
   ============================================ */
.blocking-overlay {
    position: fixed;
    inset: 0;
    z-index: 1052;
    display: flex;
    align-items: center;
    justify-content: center;
    /* Lighter scrim (0.55 → 0.45) plus a blur. This is the last thing a buyer sees
       before Stripe takes over, so the page receding out of focus reads as "handing
       off" rather than "something went dark". */
    background-color: rgba(0, 0, 0, 0.45);
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
    /* Appeared instantly before. A 0.2s fade is the difference between a modal that
       arrives and one that flashes. */
    animation: blockingOverlayFadeIn 0.2s ease;
}

/* Heavier scrim where blur isn't supported — without it, 0.45 alone doesn't separate
   the dialog from the page enough. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .blocking-overlay {
        background-color: rgba(0, 0, 0, 0.6);
    }
}

@keyframes blockingOverlayFadeIn {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.blocking-overlay__content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1.2rem;
    padding: 2rem 2.5rem;
    background: white;
    border-radius: 1.6rem;
    /* px → rem, and split into two layers: a tight contact shadow plus a wide ambient
       one. A single 30px blur reads as fog; two layers read as an object above a
       surface. Same recipe as the bottom-nav pill. */
    box-shadow: 0 0.2rem 0.6rem rgba(0, 0, 0, 0.12), 0 0.8rem 3rem rgba(0, 0, 0, 0.22);
    animation: blockingOverlayRise 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

@keyframes blockingOverlayRise {
    from {
        opacity: 0;
        transform: translateY(0.8rem) scale(0.98);
    }

    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.blocking-overlay__content .spinner-border {
    width: 3rem;
    height: 3rem;
    color: var(--color-primary);
}

.blocking-overlay__text {
    font-size: 1.4rem;
    font-weight: 600;
    color: var(--color-dark-text);
    text-align: center;
}

/* The spinner itself keeps turning — it's the only signal that the app hasn't frozen,
   so suppressing it would be worse than the motion it causes. Only the entrance
   animations are dropped. */
@media (prefers-reduced-motion: reduce) {
    .blocking-overlay,
    .blocking-overlay__content {
        animation: none;
    }
}