/* Global search modal (⌘K / Ctrl+K)
   Colors come from the --portal-c-* token layer (portal-components.css),
   which flips with the global theme — so this file needs almost no
   [data-portal-theme] overrides. Shadows are neutral ink and stay literal. */

/* ── Dialog positioning ─────────────────────────────────────────────── */
.portal-search-dialog {
    /* The LAUNCHPAD widened this (v3.1.1): the palette carries the whole
       tool grid on an empty query, and 600px showed one column of what is
       now the portal's only navigation. Height caps so the grid scrolls
       inside the dialog, never the page.

       880 → 1080 on 2026-08-26, with the grid: 880 held exactly three
       columns of the auto-fill pack, and the fourth is the difference
       between scrolling the registry and seeing it. Still a dialog and not
       a sheet — Bootstrap's own margins take it down to the viewport on
       anything narrower. */
    max-width: 1080px;
    margin: 48px auto 0;
}

/* The public launcher has one compact category and no live search results.
   Keep it modal-sized; the signed-in palette earns the wider workspace. */
.portal-search-dialog--browse {
    max-width: 52rem;
}

.portal-search-dialog .modal-body {
    display: flex;
    flex-direction: column;
    max-height: min(82dvh, 56rem);
}

/* Phone-only page chrome; desktop keeps the compact dialog entrance. */
.portal-search-phone-head {
    display: none;
}

/* Dedicated phone Menu route. It shares the launcher's content/controller,
   but participates in normal document flow instead of creating another
   viewport and another safe-area calculation inside a modal. */
.portal-menu-page {
    padding-inline: 0 !important;
}

.portal-search-page {
    min-height: calc(100dvh - var(--portal-chrome-offset-top) - var(--portal-chrome-offset-bottom));
    background: transparent;
}

/* Directory pages sit directly on the themed body canvas. Painting another
   opaque body colour here turned the phone view into a flat black rectangle
   while the rest of the portal retained its depth and theme atmosphere. */
.portal-menu-page.portal-page-themed,
.tools-page.portal-page-themed {
    background: transparent;
}

.portal-search-page .portal-search-phone-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 3.25rem;
    padding: 0.5rem 0.875rem 0.625rem;
}

.portal-search-page .portal-search-scroll {
    overflow: visible;
}

.portal-menu-appearance .portal-appearance-toggle {
    min-width: 2.75rem;
    min-height: 2.75rem;
}

/* ── The ONE scrollport ──────────────────────────────────────────────
   Recent and the tile grid are a single continuous list, Spotlight-style.
   They used to be two competing flex scrollers inside a height-capped
   `.modal-body`, and the results box (default `flex-shrink: 1`) got squeezed
   BELOW its own content height — that was the Recent sliver, and the row
   sliced in half at the point where the grid began. On a phone Bootstrap
   added a third scroller on `.modal-body` itself.

   `flex: 1 1 auto` on a child of a content-sized `.modal-body` is what the
   results box's old comment warns collapsed to 16px. It is safe here for the
   same reason it was already safe for `.portal-launchpad`: this wrapper
   always contains the server-rendered tile grid, so its content height is
   never ~0. The results box was empty at load, which is what made it
   collapse. */
.portal-search-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* ── The Launchpad — the browse grid on an empty query ──────────────── */
/* Padding only. The scrolling moved to `.portal-search-scroll` below, which
   wraps this and the results list as one continuous region. */
.portal-launchpad {
    padding: 0.75rem 1rem 0.5rem;
}

/* Give the public category a clear floor before the keyboard footer. With
   only one row of cards, the shared compact padding read as accidental. */
.portal-search-dialog--browse .portal-launchpad {
    padding-bottom: 1rem;
}

.portal-launchpad[hidden] {
    display: none;
}

/* Tighter than the /tools page — a palette is denser than a directory.
   Between sections, not after the last one: the pack spaces itself with
   `.tools-category + .tools-category` now (portal-kit.css), and a trailing
   margin here just padded the scroll container's floor. */
.portal-launchpad .tools-category + .tools-category {
    margin-top: 0.875rem;
}

/* …and the Now group is the one seam that rule cannot reach.
 *
 * It is a partial with a mount of its own (site/now-group.twig), so its
 * `.tools-category` and the launchpad grid's first one live in DIFFERENT
 * parents and are not siblings at all — "Event Day" sat flush against the
 * bottom of the Cages tile while every other section below it was spaced.
 * Reported from the palette, on a developer session where Now had rows.
 *
 * The general sibling combinator, not `+`: now-group.twig emits its <script>
 * tag straight after the mount, so the grid is the mount's second element
 * sibling and `+` matches nothing. That is also why this was easy to miss —
 * the obvious fix looks right and does nothing.
 *
 * Guarded on :not([hidden]) because the group is ABSENT rather than empty for
 * a session with no live sections (now-group.js sets the property back), and
 * a gap under a group that is not there is the same bug pointing the other
 * way. */
.portal-launchpad #portal-now-group:not([hidden]) ~ .tools-grid > .tools-category:first-child {
    margin-top: 0.875rem;
}

/* The tile for the page you are ON wears the ring — the grid answers
   "where am I" the moment it opens (portal-layout.js marks it with the
   same longest-prefix rule as the tab bar and the bar breadcrumb). */
.portal-launchpad .tools-card.is-active {
    border-color: var(--portal-c-accent-border);
    box-shadow: var(--portal-c-edge-card), 0 0 0 1px var(--portal-c-accent-border), var(--portal-c-shadow);
}

/* Logged-out browse header (no search input to show). */
.portal-search-input-wrap--browse {
    justify-content: space-between;
    /* Match the vertical rhythm of the signed-in input + scope-chip rows.
       Without this, logging out collapsed the palette header to one cramped
       line and made the same Command-K surface visibly jump in density. */
    min-height: 4.5rem;
}

.portal-search-browse-title {
    font-weight: var(--portal-weight-label);
    font-size: var(--portal-text-md);
}

/* The palette-footer Appearance mount (phones — the system bar and its own
   mount are hidden below 768). The 'drawer' mount variant carries a text
   label; keep it quiet here. */
.portal-search-footer-appearance .portal-appearance-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    border: 0;
    background: transparent;
    color: inherit;
    font-size: var(--portal-text-xs);
    /* This mount exists FOR phones — it must clear the touch floor. */
    min-height: 2.75rem;
    padding: 0 0.5rem;
}

/* ── Modal shell ─────────────────────────────────────────────────────── */
/* Overlay glass — the ⌘K markup lives at body level (base.twig), outside the
   blurred chrome, so its own backdrop-filter samples the real page. The
   specular inset supersedes the old 1px inner ring. */
.portal-search-content {
    border-radius: var(--bs-border-radius-xl);
    border: 1px solid var(--portal-c-border);
    background: var(--portal-c-overlay);
    backdrop-filter: var(--portal-blur-overlay);
    -webkit-backdrop-filter: var(--portal-blur-overlay);
    /* Same cold-start fix as the generic modal (portal-components.css): lets
       the browser build the blur (and the heavier Chromium lens filter
       below) while the dialog is still at opacity:0, before Bootstrap's
       fade starts — otherwise the first frame(s) paint with the background
       still readable and the blur visibly catches up a beat later. */
    will-change: backdrop-filter;
    color: var(--portal-c-text);
    /* Dialog-tier edge + the hero elevation step. Both are tokens now, so
       the per-theme literal override below this rule is gone — the token
       flips with the theme on its own. */
    box-shadow: var(--portal-c-edge-dialog), var(--portal-c-elev-hero);
    overflow: hidden;
    isolation: isolate;
}

/* Hero refraction (base.twig #portal-lens) — Chromium ONLY, gated by the
   data-portal-lens attribute the base.twig snippet stamps. NOT a parse
   fallback: Safari 18+ parses backdrop-filter: url(...) but cannot render
   SVG reference filters and drops the whole chain (frost included), and
   -webkit-backdrop-filter is an alias so it cannot rescue it. MUST stay a
   literal: var() indirection would invalidate at computed-value time. */
html[data-portal-lens="on"] .portal-search-content {
    backdrop-filter: url(#portal-lens) blur(8px) saturate(140%);
}

/* The lens literal bypasses the token gates — mirror the degrade here, at
   the same specificity (0,2,1), AFTER the lens rule so the tie breaks this
   way. */
@media (prefers-reduced-transparency: reduce) {
    html[data-portal-lens="on"] .portal-search-content {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
}

html[data-portal-glass="off"] .portal-search-content {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* ── Input row ──────────────────────────────────────────────────────── */
.portal-search-input-wrap {
    display: flex;
    align-items: center;
    padding: 0 1rem;
    border-bottom: 1px solid var(--portal-c-border);
}

.portal-search-icon {
    color: var(--portal-c-text-muted);
    font-size: var(--portal-text-base);
    flex-shrink: 0;
    margin-right: .65rem;
}

.portal-search-input {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;
    color: inherit;
    font-size: var(--portal-text-base);
    padding: .85rem 0;
    caret-color: var(--portal-c-text);
}

.portal-search-input::placeholder {
    color: var(--portal-c-input-placeholder);
}

/* In-flight indicator — previous results stay on screen while a newer
   request runs; this spinner is the only "searching" signal after the
   first response has painted. */
.portal-search-inflight {
    flex-shrink: 0;
    width: 1rem;
    height: 1rem;
    border-width: 0.14em;
    margin-left: .5rem;
    color: var(--portal-c-text-muted);
}

/* Close button — the portal's standard modal dismiss (.btn-close). It
   auto-inverts for light/dark via the modal's data-bs-theme, so no theme
   overrides are needed here — just spacing and a roomy touch/PWA tap target. */
.portal-search-close {
    flex-shrink: 0;
    margin-left: .5rem;
    padding: .5rem;
}

/* ── Results list ────────────────────────────────────────────────────── */
/* NOT a scrollport any more — the cap moved up one level to
 * `.portal-search-scroll`, which is now the single scroll region for Recent
 * and the tile grid together. The reasoning the old comment here recorded
 * still applies, unchanged, one level up: the cap is viewport-relative and
 * never a flat px value, because a flat 380px stopped growing while windows
 * kept getting bigger.
 *
 * No top padding: `.portal-search-category`'s own .55rem is the gap under the
 * sigil chips, and stacking both put ~1.65rem between the chips and the word
 * RECENT. */
.portal-search-results {
    padding: 0 0 .5rem;
}

/* Loading / empty / error panel — sits OUTSIDE the listbox element so
   assistive tech never finds state text posing as an option. */
.portal-search-state .portal-search-skeletons {
    padding: .5rem 0;
}

.portal-search-skeleton-row {
    padding: .45rem 1rem;
}

.portal-search-skeleton-row .portal-skeleton {
    display: block;
}

/* Category heading */
.portal-search-category {
    font-size: var(--portal-text-xs);
    font-weight: var(--portal-weight-strong);
    text-transform: uppercase;
    letter-spacing: var(--portal-label-tracking);
    color: var(--portal-c-text-muted);
    padding: .55rem 1rem .2rem;
}

.portal-search-category-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

/* "Clear" affordance on the Recent header */
.portal-search-clear-recents {
    background: none;
    border: 0;
    padding: .1rem .25rem;
    font-size: var(--portal-text-xs);
    font-weight: var(--portal-weight-label);
    text-transform: none;
    letter-spacing: normal;
    color: var(--portal-c-text-muted);
    cursor: pointer;
}

.portal-search-clear-recents:hover {
    color: var(--portal-c-accent);
}

/* Result row */
.portal-search-result-item {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .5rem 1rem;
    cursor: pointer;
    border-radius: 0;
    transition: background-color var(--portal-dur-1, 120ms);
}

.portal-search-result-item:hover,
.portal-search-result-item.portal-search-result-active {
    background-color: var(--portal-c-row-selected);
}

.portal-search-result-icon {
    font-size: var(--portal-text-sm);
    color: var(--portal-c-text-muted);
    width: 1.1rem;
    text-align: center;
    flex-shrink: 0;
}

.portal-search-result-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.portal-search-result-label {
    font-size: var(--portal-text-md);
    font-weight: var(--portal-weight-medium);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Matched-token highlight — quiet accent wash, never browser yellow. */
.portal-search-mark {
    background: var(--portal-c-accent-muted);
    color: inherit;
    padding: 0 .08em;
    border-radius: var(--bs-border-radius-sm);
}

.portal-search-result-role {
    font-size: var(--portal-text-xs);
    color: var(--portal-c-text-muted);
    margin-left: .4rem;
    font-weight: 400;
}

.portal-search-result-sub {
    font-size: var(--portal-text-sm);
    color: var(--portal-c-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Action verbs (Actions category) ─────────────────────────────────── */
/* Same row anatomy as page results (keyboard nav is shared); the chip is the
   distinct treatment — a small kbd-style square in the theme accent. */
.portal-search-result-action-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: var(--bs-border-radius);
    flex-shrink: 0;
    border: 1px solid var(--portal-c-accent-border);
    color: var(--portal-c-accent);
    font-size: var(--portal-text-xs);
}

/* ── Footer hint bar ─────────────────────────────────────────────────── */
.portal-search-footer {
    display: flex;
    gap: 1.25rem;
    padding: .5rem 1rem;
    border-top: 1px solid var(--portal-c-border-subtle);
    font-size: var(--portal-text-xs);
    color: var(--portal-c-kbd);
}

.portal-search-footer kbd {
    font-size: var(--portal-text-2xs);
    padding: .1rem .35rem;
    color: inherit;
    border: 1px solid currentColor;
    border-radius: var(--bs-border-radius-sm);
    background: transparent;
    font-family: var(--portal-font-mono);
}

/* The Shortcuts hint is a real button, not a legend — it reads as one of the
 * hints beside it until you hover, which is when it admits it is clickable. */
.portal-search-footer-keys {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.portal-search-footer-keys:hover,
.portal-search-footer-keys:focus-visible {
    color: var(--portal-c-text);
}

/* ── Responsive: full-screen sheet on phones ─────────────────────────
   modal-fullscreen-sm-down (the template) owns the geometry below 576 —
   since v3.1.1 the palette is the whole navigation, and a phone deserves
   the whole screen for it. The body column just has to fill it. */
@media (max-width: 575.98px) {
    /* Bootstrap's .modal-fullscreen-sm-down resets max-width to none and
       margin to 0 — but `.portal-search-dialog` above is the SAME specificity
       (0,1,0) and this file loads after Bootstrap, so it won the tie and the
       phone dialog kept `margin: 48px auto 0` on a 100%-height box: shoved
       down 48px, a strip of page showing above the search bar, and its bottom
       48px hanging off the fold. Width was never contested, which is why the
       phone test (asserting width only) never saw it. Restore the template's
       intent. */
    .portal-search-dialog {
        max-width: none;
        margin: 0;
    }

    .portal-search-dialog .modal-body {
        max-height: none;
        height: 100%;
        /* Bootstrap makes the body a scroller at this width. The wrapper is
           the one scrollport; a second one here is what sliced rows. */
        overflow: hidden;
    }

    /* A fullscreen launcher should read as a native page, not translucent
       desktop glass stretched to the physical edges of the phone. */
    .portal-search-content {
        border: 0;
        border-radius: 0;
        background: transparent;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        box-shadow: none;
    }

    .portal-search-phone-head {
        display: flex;
        flex: 0 0 auto;
        align-items: flex-end;
        justify-content: space-between;
        min-height: calc(3.25rem + var(--portal-safe-top, 0px));
        padding: var(--portal-safe-top, 0px) 0.875rem 0.625rem;
    }

    .portal-search-page .portal-search-phone-head {
        align-items: center;
        min-height: 3.25rem;
        padding: 0.5rem 0.875rem 0.625rem;
    }

    .portal-search-page .portal-launchpad {
        padding-bottom: 1rem;
    }

    .portal-search-phone-title {
        display: inline-flex;
        align-items: center;
        gap: 0.5rem;
        font-size: var(--portal-text-lg);
        font-weight: var(--portal-weight-strong);
    }

    .portal-search-phone-title i {
        color: var(--portal-c-accent);
    }

    .portal-search-input-wrap {
        min-height: 2.75rem;
        margin: 0 0.75rem 0.375rem;
        padding: 0 0.75rem;
        border: 1px solid var(--portal-c-edge-control);
        border-radius: var(--bs-border-radius-lg);
        background: var(--portal-c-input-bg);
    }

    .portal-search-input-wrap > .portal-search-close {
        display: none;
    }

    .portal-search-input-wrap--browse {
        display: none;
    }

    .portal-search-input {
        padding: 0.625rem 0;
    }

    /* This footer is the bottom edge of the fullscreen phone sheet. Keep its
       Shortcuts control above the home indicator instead of letting iOS crop
       the label and tap target into the physical safe-area strip. */
    /* Scope sigils are a command-palette teaching aid. On a phone they became
       a clipped second toolbar between the one search field and its results;
       typing the sigil still works for anyone who already knows it. */
    .portal-search-content .portal-search-sigils {
        display: none;
    }

    /* The live floor belongs on Home. Repeating its four large status cards
       above the phone's actual directory made Menu mostly a second dashboard. */
    .portal-launchpad #portal-now-group {
        display: none !important;
    }

    .portal-launchpad #portal-now-group ~ .tools-grid > .tools-category:first-child {
        margin-top: 0;
    }

    /* A phone has no useful keyboard-shortcut story. Do not reserve a large
       fixed footer for that one control; logged-out Appearance remains. */
    .portal-search-footer-keys {
        display: none;
    }

    .portal-search-footer:not(:has(.portal-search-footer-appearance)) {
        display: none;
    }

    .portal-search-footer:has(.portal-search-footer-appearance) {
        justify-content: flex-end;
        padding-bottom: calc(0.5rem + var(--portal-safe-bottom, 0px));
    }

    .portal-launchpad {
        padding: 0.5rem 0.75rem calc(1rem + var(--portal-safe-bottom, 0px));
    }

    .portal-launchpad .tools-card-grid {
        gap: 0.625rem;
    }

    .portal-launchpad .tools-card {
        min-height: 3.5rem;
        padding: 0.625rem 0.75rem;
    }

    .portal-launchpad .tools-card-label {
        font-size: var(--portal-text-md, 0.95rem);
    }
}

/* The ↑↓/↵ hints are keyboard-only guidance — meaningless on touch, where
   the × closes. The footer BAR survives on touch phones though: it carries
   the Appearance control there (the system bar and its mount are hidden
   below 768). On touch tablets (≥768) the bar has nothing left to show. */
@media (hover: none) {
    .portal-search-footer > span:not(.portal-search-footer-appearance) {
        display: none;
    }

    /* The chips were ~18px tall and the result rows ~34px, both well under the
       44px touch floor — the other half of "unreadable on a small screen".
       Pointer devices keep today's density. Same precedent as
       `.portal-search-footer-appearance .portal-appearance-toggle`, the one
       palette control that already clears the floor.

       The chip rule is parent-scoped on purpose. A media query adds no
       specificity, and `.portal-search-sigil`'s own padding is declared LATER
       in this file (the Search modes section below) — measured, a bare
       `.portal-search-sigil` here lost the tie and the chips stayed 22px on a
       phone while the rows correctly grew. (0,2,0) wins regardless of order,
       so moving either block later cannot quietly undo this again. */
    .portal-search-sigils .portal-search-sigil {
        padding: 0.35rem 0.7rem;
    }

    .portal-search-result-item {
        padding: 0.7rem 1rem;
    }
}

@media (hover: none) and (min-width: 768px) {
    .portal-search-footer {
        display: none;
    }
}


/* ── Search modes ───────────────────────────────────────────────────────
   The visible half of the sigil grammar. Quiet at rest — they are a hint,
   not a toolbar — and the one you are in wears the accent, because the
   panel below is showing you less than everything and should say so. */
.portal-search-sigils {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    /* Balanced, and the top value is the point: it was zero, so the chips
       butted straight against the input's bottom border while three paddings
       stacked underneath them. */
    padding: 0.5rem 1rem 0.5rem;
}

.portal-search-sigil {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.1rem 0.5rem;
    border: 1px solid var(--portal-c-edge-control-subtle);
    border-radius: var(--bs-border-radius-pill);
    background: transparent;
    color: var(--portal-c-text-muted);
    font-size: var(--portal-text-2xs);
    cursor: pointer;
    transition: color var(--portal-dur-1) var(--portal-ease-out),
                background-color var(--portal-dur-1) var(--portal-ease-out),
                border-color var(--portal-dur-1) var(--portal-ease-out);
}

.portal-search-sigil:hover,
.portal-search-sigil:focus-visible {
    color: var(--portal-c-text);
    background: var(--portal-c-row-hover);
}

.portal-search-sigil.is-on {
    color: var(--portal-c-accent);
    background: var(--portal-c-accent-muted);
    border-color: var(--portal-c-accent-border);
}

.portal-search-sigil kbd {
    font-size: var(--portal-text-2xs);
    padding: 0 .25rem;
    color: inherit;
    border: 0;
    background: transparent;
    font-family: var(--portal-font-mono);
    font-weight: var(--portal-weight-strong);
}
