/* ═════════════════════════════════════════════════════════════════════════
   app-home.css — /app, the app-mode home.

   Loaded ONLY by app/Views/app-home.php. Nothing here can reach another page,
   which is deliberate: this is the one screen on the site that is allowed to
   look like an app rather than a website.

   ⚠️ COLOURS COME FROM THE THEME, NOT FROM HERE. Every value below is a
   --theme-* variable set by themes.css, so /app follows light / dark /
   high-contrast like everything else. Hard-coding a palette here would give
   the app a fifth rendering nobody measures. (SKILLS 1c — geometry is ours,
   the palette belongs to the theme.)
   ═════════════════════════════════════════════════════════════════════════ */

body.wsp-app-home {
    background: var(--theme-bg, #f7f8fa);
    color: var(--theme-text, #1f2430);
    /* An app scrolls its list, not its chrome. */
    overscroll-behavior-y: contain;
}

/* ⛔ THE COFFEE TAB IS GONE FROM /app UNCONDITIONALLY, not via html.wsp-app.
   app-mode.css hides it when the app-mode class is set — but that class only
   appears inside the installed PWA or with ?app=1, and /app opened in a plain
   browser has neither. It was still floating over the list (caught in a
   screenshot, not by a check). This page IS the app shell by definition, so it
   does not need to be told it is in app mode. */
body.wsp-app-home #wspSupportFloat { display: none !important; }

/* ── the top bar ─────────────────────────────────────────────────────────
   Sticky, not fixed: sticky keeps it in normal flow, so the list below needs
   no magic top padding that would have to be kept in step with the bar's
   height every time a control is added to it.

   ⚠️ `position: sticky` is inert site-wide because body carries
   `overflow-x: hidden` (recorded in the blog-theming memory) — that rule
   lives in site.css and applies to `body`, so it is overridden here for this
   page only, where there is no wide content to clip. */
body.wsp-app-home { overflow-x: visible; }

.wsp-app-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    padding: .55rem .85rem .5rem;
    background: var(--theme-surface, #fff);
    border-bottom: 1px solid var(--theme-border, #e4e7ec);
}

.wsp-app-bar-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: .55rem;
}

.wsp-app-brand {
    display: inline-flex;
    align-items: center;
    gap: .5rem;
    font-weight: 700;
    font-size: 1.02rem;
    letter-spacing: -.01em;
    /* ⛔ SURFACE-TEXT, not --theme-text: the bar is painted --theme-surface, and
       in high-contrast BOTH of those tokens are #ffffff (that theme puts white
       text on a BLACK page and black ink on WHITE surfaces). --theme-text here
       is invisible in exactly the theme chosen for legibility. */
    color: var(--theme-surface-text, #1f2430);
}

.wsp-app-brand img { border-radius: 6px; }

/* ⭐ .wsp-app-bar-btn AND THE THEME MENU NOW LIVE IN app-mode.css.
   The owner asked (2026-08-28) for the theme control on every app screen, not
   just this one, so the markup became partials/app/theme-menu.php and the
   picker pages' app topbar includes it too. Those pages load app-mode.css and
   never load THIS file, so the rules had to move to the sheet both pages get.
   ⛔ Copying them instead would have been two stylesheets that are "equivalent"
   until the day one of them isn't (SKILLS 38). Nothing here styles them now. */

/* ── search ──────────────────────────────────────────────────────────── */
.wsp-app-search {
    position: relative;
    display: flex;
    align-items: center;
}

.wsp-app-search > .bi-search {
    position: absolute;
    left: .8rem;
    font-size: .95rem;
    opacity: .55;
    pointer-events: none;
}

.wsp-app-search input {
    width: 100%;
    /* 16px minimum: anything smaller makes iOS Safari zoom the viewport on
       focus, which on an app home is a jarring first interaction. */
    font-size: 16px;
    padding: .6rem .75rem .6rem 2.35rem;
    border-radius: 11px;
    border: 1px solid var(--theme-border, #e4e7ec);
    background: var(--theme-bg, #f2f3f7);
    color: var(--theme-text, #1f2430);
    -webkit-appearance: none;
    appearance: none;
}

.wsp-app-search input:focus {
    outline: none;
    border-color: var(--theme-accent, #6366f1);
    box-shadow: 0 0 0 3px rgba(99, 102, 241, .16);
}

/* The browser's own clear affordance is inconsistent and tiny; ours is a real
   target and works the same everywhere. */
.wsp-app-search input::-webkit-search-cancel-button { display: none; }

.wsp-app-search-clear {
    position: absolute;
    right: .35rem;
    width: 34px;
    height: 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--theme-muted, #6b7280);
    font-size: .85rem;
}

/* ── the grouping toggle ─────────────────────────────────────────────── */
/* The row that carries the segmented control. It held a "Sort by:" label until
   2026-08-28, when the owner cut it; the wrapper stays because it is what the
   width cap at the bottom of this file applies to — the control itself is a
   flex CHILD and capping a child does not place it. */
.wsp-app-sort {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-top: .55rem;
}

.wsp-app-sort .wsp-app-seg { flex: 1 1 auto; margin-top: 0; }

.wsp-app-seg {
    display: flex;
    gap: .3rem;
    margin-top: .55rem;
    padding: .22rem;
    border-radius: 11px;
    background: var(--theme-bg, #f2f3f7);
    border: 1px solid var(--theme-border, #e4e7ec);
}

.wsp-app-seg-btn {
    flex: 1 1 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .38rem;
    min-height: 36px;
    border: 0;
    border-radius: 9px;
    background: transparent;
    color: var(--theme-muted, #6b7280);
    font-size: .875rem;
    font-weight: 600;
}

.wsp-app-seg-btn.is-active {
    background: var(--theme-surface, #fff);
    color: var(--theme-surface-text, #1f2430);
    box-shadow: 0 1px 2px rgba(16, 24, 40, .08);
}

/* ── the list ────────────────────────────────────────────────────────── */
.wsp-app-main { padding: .7rem .85rem 2.5rem; }

/* ⛔ THE CARD SETS ITS OWN INK. Without this it inherits `--theme-text` from
   body — which is correct against the PAGE background but wrong against a
   surface, and in high-contrast the two are both #ffffff, so every family and
   picker name rendered white on a white card. A surface must always declare the
   ink that belongs to it; inheriting the page's is the bug. */
/* the family's own hue, so the open-state bar and title match its rows */
.wsp-app-fam[data-family="wheel"]        { --wsp-fam-head: #6366f1; }
.wsp-app-fam[data-family="center-spin"]  { --wsp-fam-head: #0ea5e9; }
.wsp-app-fam[data-family="combo"]        { --wsp-fam-head: #8b5cf6; }
.wsp-app-fam[data-family="reels"]        { --wsp-fam-head: #a855f7; }
.wsp-app-fam[data-family="boxes"]        { --wsp-fam-head: #f59e0b; }
.wsp-app-fam[data-family="cards"]        { --wsp-fam-head: #ec4899; }
.wsp-app-fam[data-family="boards"]       { --wsp-fam-head: #14b8a6; }
.wsp-app-fam[data-family="maps"]         { --wsp-fam-head: #0891b2; }
.wsp-app-fam[data-family="arcade"]       { --wsp-fam-head: #ef4444; }
.wsp-app-fam[data-family="cams"]         { --wsp-fam-head: #22c55e; }
.wsp-app-fam[data-family="party"]        { --wsp-fam-head: #f97316; }
.wsp-app-fam[data-family="wizarding"]    { --wsp-fam-head: #7c3aed; }
.wsp-app-fam[data-family="carousel"]     { --wsp-fam-head: #ca8a04; }
.wsp-app-fam[data-family="draw"]         { --wsp-fam-head: #3b82f6; }
.wsp-app-fam[data-family="calm"]         { --wsp-fam-head: #10b981; }
.wsp-app-fam[data-family="render"]       { --wsp-fam-head: #d946ef; }
.wsp-app-fam[data-family="coin-dice"]    { --wsp-fam-head: #64748b; }

.wsp-app-fam {
    margin-bottom: .55rem;
    border-radius: 14px;
    background: var(--theme-surface, #fff);
    color: var(--theme-surface-text, #1f2430);
    border: 1px solid var(--theme-border, #e4e7ec);
    overflow: hidden;
}

/* ── the family header ────────────────────────────────────────────────────
   ⛔ THREE CONTROLS IN A ROW, NOT ONE BUTTON (owner, 2026-08-28). The main area
   is an <a> to the flagship picker on the three families that name one, and a
   <button> everywhere else; the chevron always toggles. That is why this is a
   plain div — a <button> cannot legally contain a link, and the header had to
   hold both. */
.wsp-app-fam-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding-right: .35rem;
}

/* The main area carries the art and the text and stretches; both element types
   are reset to look identical so the list does not betray which is which. */
.wsp-app-fam-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .7rem .35rem .7rem .75rem;
    background: transparent;
    border: 0;
    text-align: left;
    text-decoration: none;
    color: inherit;
    font: inherit;
}

.wsp-app-fam-main:active { background: var(--theme-bg, #f2f3f7); }

/* ⚠️ 44px, because the chevron is now the ONLY guaranteed way to expand a
   family and a cramped one would strand anyone on the 13 families whose header
   is not a link. */
.wsp-app-fam-toggle {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 10px;
    background: transparent;
    color: var(--theme-muted, #6b7280);
}

.wsp-app-fam-toggle:active { background: var(--theme-bg, #f2f3f7); }

/* The stage preview — the owner's core ask: see what the engine looks like
   before you tap it. The <img> removes ITSELF on error (see picker-list.php),
   uncovering the icon underneath, so a family whose webp has not been
   generated yet degrades to an icon rather than a broken-image box. */
.wsp-app-fam-art {
    position: relative;
    flex: 0 0 auto;
    width: 60px;
    height: 46px;
    border-radius: 9px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--theme-bg, #f2f3f7);
    color: var(--theme-accent-ink, #4f46e5);
    font-size: 1.25rem;
}

/* ⚠️ HIDDEN UNTIL IT HAS ACTUALLY LOADED. The icon is painted underneath, so
   an image that 404s, is still lazy, or never decodes leaves the icon showing
   rather than an empty box covering it. `.is-loaded` is added by the img's own
   onload — see the note in picker-list.php for why this is keyed on success
   rather than on error. */
/* ⛔ `contain`, NOT `cover`. The previews are 360x228 (ratio 1.58) and this box
   is 60x46 (ratio 1.30), so `cover` cropped the top and bottom off every one —
   the classic wheel arrived as a horizontal band with its rim sliced away
   (owner, 2026-08-27: "classic wheel spin image is cut from top/bottom").
   `contain` fits the whole stage inside the box and letterboxes the difference
   against the tile's own background, which is what a thumbnail of a stage
   wants: the shape is the information. */
.wsp-app-fam-art img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: 0;
    transition: opacity .18s ease;
}

.wsp-app-fam-art img.is-loaded { opacity: 1; }

.wsp-app-fam-text { flex: 1 1 auto; min-width: 0; }

.wsp-app-fam-title {
    display: block;
    font-weight: 700;
    font-size: .97rem;
    line-height: 1.25;
}

.wsp-app-fam-desc {
    display: block;
    font-size: .78rem;
    line-height: 1.3;
    color: var(--theme-muted, #6b7280);
    /* Two lines, then stop. A family blurb that runs to four lines turns the
       list back into the wall of text this page exists to replace. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.wsp-app-fam-count {
    flex: 0 0 auto;
    min-width: 26px;
    text-align: center;
    font-size: .74rem;
    font-weight: 700;
    padding: .12rem .4rem;
    border-radius: 20px;
    color: var(--theme-muted, #6b7280);
    background: var(--theme-bg, #f2f3f7);
}

.wsp-app-fam-chev {
    flex: 0 0 auto;
    font-size: .8rem;
    opacity: .5;
    transition: transform .18s ease;
}

.wsp-app-fam-head.is-open .wsp-app-fam-chev { transform: rotate(180deg); }

/* ⛔ THE OPEN FAMILY HAS TO BE OBVIOUS WITHOUT READING THE CARET (owner,
   2026-08-28): *"current opened list should have a slightly different color or
   something.. i am getting confused even myself so many times to figure out
   which one is open.. so i have to look at caret sign to know"*.

   A rotated chevron is a 12px glyph at the far right of a 64px row — it is the
   smallest possible signal for the single most important piece of state on the
   screen. This tints the whole open header and puts a bar of the family's own
   colour down its leading edge, so the answer is visible in peripheral vision.

   ⚠️ --theme-bg, NOT a hard-coded tint: the header sits on --theme-surface, and
   the page's own background is by definition the shade that reads as "recessed"
   against it in every theme, including high-contrast where both are forced.
   ⚠️ The bar is an inset box-shadow rather than a border-left so it adds no
   width — a 3px border would shift the art and re-break the alignment fixed
   earlier today. */
.wsp-app-fam-head.is-open {
    background: var(--theme-bg, #f2f3f7);
    box-shadow: inset 3px 0 0 0 var(--wsp-fam-head, var(--theme-accent, #6366f1));
}

/* ⛔ THE OPEN HEADER'S INK IS NOT RECOLOURED, AND THE BACKGROUND TINT HAS A
   COST. First version painted the title in the family hue and let the desc keep
   --theme-muted. Measured, and all three failed AA:

       family title  #6366f1 on #f2f3f7   4.03:1   (needs 4.5)
       family desc   #6b7280 on #f2f3f7   4.36:1
       family title  #6366f1 on #121212   4.19:1   (dark)

   The hue is tuned to sit on a SURFACE, not on the page ground, and tinting the
   background at all costs the muted ink the margin it was passing on. So the
   colour identity moves entirely to the 3px bar — which is decorative and has no
   contrast duty — and the open header darkens only its secondary ink to pay for
   its own tint. Same lesson as --theme-surface-text: an ink belongs to a ground.
*/
/* ⚠️ AND THE DARKER INK IS LIGHT-THEME ONLY. Applied unscoped it failed the
   other two grounds immediately — #565d6b measured 2.83:1 on dark's #121212 and
   3.17:1 on high-contrast's #000. In those themes the page ground is DARKER
   than the card, so the open header's tint moves the text toward more contrast,
   not less, and their existing inks already pass. One ink cannot serve two
   themes (SKILLS 86). */
body:not([data-theme="dark"]):not([data-theme="high-contrast"]) .wsp-app-fam-head.is-open .wsp-app-fam-desc,
body:not([data-theme="dark"]):not([data-theme="high-contrast"]) .wsp-app-fam-head.is-open .wsp-app-fam-count {
    color: #565d6b;
}

.wsp-app-fam-body { border-top: 1px solid var(--theme-border, #e4e7ec); }

/* ── a picker row ────────────────────────────────────────────────────── */
.wsp-app-tool {
    display: flex;
    align-items: center;
    /* ⚠️ THE SAME GAP AS .wsp-app-fam-main, and it has to stay that way. With
       the art boxes matched, the gap is the only thing left that can push a row
       title out of line with the family title above it — it was .8 against .75
       and showed up as a 0.8px drift. */
    gap: .75rem;
    /* 64px since 2026-08-28. The owner compared this list to three competitor
       apps and called ours "small and dull" beside them; at 52px with a 26px
       flat glyph the rows read as a settings menu rather than a shelf of
       things to play with. */
    min-height: 64px;
    padding: .5rem .75rem;
    text-decoration: none;
    color: inherit;
    border-bottom: 1px solid var(--theme-border, #eef0f4);
}

.wsp-app-tool:last-child { border-bottom: 0; }
.wsp-app-tool:active { background: var(--theme-bg, #f2f3f7); }

/* ── THE ROW MARK ────────────────────────────────────────────────────────
   ⛔ ONE LANGUAGE, ONE SIZE, ONE SOURCE OF COLOUR.

   This list used to mix four kinds of glyph — an emoji (76 rows), an official
   brand <img> (7), a drawn <svg> and a bootstrap <i> — at four different sizes
   (15 / ~18 / 19 / 24px) and in four visual styles. The owner's verdict was
   *"some icons colored... some logos... some professional looking icons...
   some emojis"*, and matching only the SIZES did not fix it, because the
   problem was the mixture itself.

   picker-list.php now renders one curated mark per row (PickerIcons) and this
   tints it by engine family. No tile, per the owner's choice of "C — no tile",
   so the colour is the whole signal.

   ⚠️ A PAST TRAP WORTH KEEPING: the old brand rule was
   `.wsp-app-tool-icon--brand svg`, and social-brand-icon.php emits an `<img>`.
   It matched nothing, looked entirely reasonable, and is why those logos stayed
   oversized (SKILLS 120). */
/* ⭐ THE ROW ART IS THE PICKER'S REAL STAGE (owner, 2026-08-28), and it is the
   SAME BOX as the family card's art on purpose.

   ⛔ THAT SAMENESS IS THE ALIGNMENT FIX. He reported the family image sitting
   "on the right while other list images are a little to the left". Measured:
   both boxes already began at left 26.6 — the left edges were never wrong. What
   differed was WIDTH (60 vs 46), so the artwork's CENTRE sat 7px apart and the
   titles beside them 13.2px apart, which is what the eye actually reads as
   misalignment. Matching the box fixes both, and it is also what "make the
   image bigger" asked for. ⚠️ So they must keep matching — including inside the
   480px media query, where the family art grows to 74x52.

   The mark stays underneath as the fallback for any picker whose stage could
   not be shot; `is-loaded` reveals the image over it (onload, never onerror —
   see picker-list.php). */
.wsp-app-tool-icon {
    position: relative;
    flex: 0 0 auto;
    width: 60px;
    height: 46px;
    border-radius: 9px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.25rem;
    line-height: 1;
    background: var(--theme-bg, #f2f3f7);
    color: var(--wsp-fam, var(--theme-accent-ink, #4f46e5));
}

.wsp-app-tool-icon img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    opacity: 0;
    transition: opacity .18s ease;
}

.wsp-app-tool-icon img.is-loaded { opacity: 1; }


/* ⛔ `!important` HERE IS NOT LAZINESS — IT IS ANSWERING ONE.
   themes.css:269 paints EVERY bootstrap glyph site-wide:

       body[data-theme] .bi { color: var(--theme-icon) !important; opacity: … }

   so in dark mode all 144 font marks rendered in the theme's near-white icon
   colour and the family palette below did nothing. Only the 7 DRAWN marks
   looked right, because an inline SVG uses `fill: currentColor` and inherits
   from the span instead.

   ⚠️ Matching `!important` is not enough on its own — the winner between two
   important declarations is the more specific selector. This one is
   0,3,1 against the theme rule's 0,2,1, and it is scoped to the app list so
   nothing else on the site changes. `inherit` then takes the family hue from
   the span, keeping ONE source for both mark kinds. */
.wsp-app-tool .wsp-app-tool-icon > i.bi {
    font-size: 20px;
    line-height: 1;
    color: inherit !important;
    opacity: 1 !important;
}

.wsp-app-tool-icon > svg {
    width: 20px;
    height: 20px;
}

/* ── THE FAMILY PALETTE ──────────────────────────────────────────────────
   One hue per engine family, so scrolling the list you can SEE where one
   engine type ends and the next begins — the colour carries the same meaning
   as the grouping the list is built on.

   ⚠️ These are deliberately mid-tone values that hold up on BOTH grounds: the
   list renders on white in light and on #1e1e1e in dark, and a hue tuned for
   one washes out or muddies on the other. They are decoration, not text, so
   they carry no AA obligation — but they are still kept clear of both extremes
   so a mark never disappears into its background. */
/* ⚠️ TWO TONES PER FAMILY NOW, as custom properties rather than a bare
   `color`. The deep tone is the glyph and the wheel's dark slices; the pale
   tone is the alternating slice. Both are declared explicitly rather than
   derived with color-mix(), which is not safe on every Android WebView this
   app will land on — a failed color-mix is an invalid declaration and the
   slice would simply vanish. */
.wsp-app-tool[data-famkey="wheel"]        { --wsp-fam: #6366f1; --wsp-fam-soft: #c7d2fe; }
.wsp-app-tool[data-famkey="center-spin"]  { --wsp-fam: #0ea5e9; --wsp-fam-soft: #bae6fd; }
.wsp-app-tool[data-famkey="combo"]        { --wsp-fam: #8b5cf6; --wsp-fam-soft: #ddd6fe; }
.wsp-app-tool[data-famkey="reels"]        { --wsp-fam: #a855f7; --wsp-fam-soft: #e9d5ff; }
.wsp-app-tool[data-famkey="boxes"]        { --wsp-fam: #f59e0b; --wsp-fam-soft: #fde68a; }
.wsp-app-tool[data-famkey="cards"]        { --wsp-fam: #ec4899; --wsp-fam-soft: #fbcfe8; }
.wsp-app-tool[data-famkey="boards"]       { --wsp-fam: #14b8a6; --wsp-fam-soft: #99f6e4; }
.wsp-app-tool[data-famkey="maps"]         { --wsp-fam: #0891b2; --wsp-fam-soft: #a5f3fc; }
.wsp-app-tool[data-famkey="arcade"]       { --wsp-fam: #ef4444; --wsp-fam-soft: #fecaca; }
.wsp-app-tool[data-famkey="cams"]         { --wsp-fam: #22c55e; --wsp-fam-soft: #bbf7d0; }
.wsp-app-tool[data-famkey="party"]        { --wsp-fam: #f97316; --wsp-fam-soft: #fed7aa; }
.wsp-app-tool[data-famkey="carousel"]     { --wsp-fam: #ca8a04; --wsp-fam-soft: #fef08a; }
/* wizarding, added 2026-08-28 with the 17th family. A deep violet — the one gap
   left in the palette, and far enough from `party` (#f97316) and `render`
   (#d946ef) that the check for distinct family colours still passes. */
.wsp-app-tool[data-famkey="wizarding"]    { --wsp-fam: #7c3aed; --wsp-fam-soft: #ddd6fe; }
.wsp-app-tool[data-famkey="draw"]         { --wsp-fam: #3b82f6; --wsp-fam-soft: #bfdbfe; }
.wsp-app-tool[data-famkey="calm"]         { --wsp-fam: #10b981; --wsp-fam-soft: #a7f3d0; }
.wsp-app-tool[data-famkey="render"]       { --wsp-fam: #d946ef; --wsp-fam-soft: #f5d0fe; }
.wsp-app-tool[data-famkey="coin-dice"]    { --wsp-fam: #64748b; --wsp-fam-soft: #cbd5e1; }

/* Slate reads as "disabled" on a dark ground, so coin-dice gets a lift there;
   likewise the yellow-leaning carousel hue needs more light in dark mode. */
:root[data-theme="dark"] .wsp-app-tool[data-famkey="coin-dice"],
body[data-theme="dark"]  .wsp-app-tool[data-famkey="coin-dice"] { --wsp-fam: #94a3b8; --wsp-fam-soft: #475569; }
:root[data-theme="dark"] .wsp-app-tool[data-famkey="carousel"],
body[data-theme="dark"]  .wsp-app-tool[data-famkey="carousel"] { --wsp-fam: #eab308; --wsp-fam-soft: #713f12; }

.wsp-app-tool-text { flex: 1 1 auto; min-width: 0; }

.wsp-app-tool-title {
    display: block;
    font-size: .92rem;
    font-weight: 700;
    line-height: 1.25;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.wsp-app-tool-badge {
    display: block;
    font-size: .74rem;
    line-height: 1.3;
    color: var(--theme-muted, #6b7280);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.wsp-app-tool-go { flex: 0 0 auto; font-size: .78rem; opacity: .35; }

/* Search results are a flat list, so they get the card treatment the family
   bodies get from their wrapper. */
#wspAppSearchResults {
    border-radius: 14px;
    overflow: hidden;
    background: var(--theme-surface, #fff);
    color: var(--theme-surface-text, #1f2430);   /* same reason as .wsp-app-fam */
    border: 1px solid var(--theme-border, #e4e7ec);
}

.wsp-app-empty {
    text-align: center;
    padding: 2.5rem 1rem;
    color: var(--theme-muted, #6b7280);
    font-size: .9rem;
}

.wsp-app-empty .bi { display: block; font-size: 1.6rem; margin-bottom: .5rem; opacity: .5; }

/* ── high contrast ───────────────────────────────────────────────────────
   ⛔ THIS BLOCK EXISTS BECAUSE themes.css WINS HERE, AND FIGHTING IT IS WRONG.

   `body[data-theme="high-contrast"] section` sets
   `background:#000 !important; color:#fff !important` (themes.css:850) — and
   the family/category cards ARE `<section>` elements, so in that one theme they
   are black with white text no matter what this file says. Measured, not
   assumed: `.wsp-app-fam` computes `rgb(0,0,0)` / `rgb(255,255,255)` there.

   That is the theme working as intended, and a card of pure black under pure
   white text is exactly what someone picking "high contrast" asked for. What
   BREAKS is the secondary ink: `--theme-muted` is #1f2937 — a DARK grey chosen
   for light grounds — which measured **1.43:1 on that forced black**. So the
   fix is to repaint the muted inks for this theme, not to re-assert a
   background the theme has deliberately overridden.

   The bar is pinned explicitly for the same reason: leaving it to
   `--theme-surface` gave black-on-black for the brand (1:1). */
body[data-theme="high-contrast"] .wsp-app-bar {
    background: #000;
    border-bottom-color: #fff;
}

body[data-theme="high-contrast"] .wsp-app-brand,
body[data-theme="high-contrast"] .wsp-app-fam-title,
body[data-theme="high-contrast"] .wsp-app-tool-title {
    color: #fff;
}

/* #e5e7eb on #000 measures ~17:1 — comfortably AA, and still reads as the
   quieter of the two inks beside pure white. */
body[data-theme="high-contrast"] .wsp-app-fam-desc,
body[data-theme="high-contrast"] .wsp-app-tool-badge,
body[data-theme="high-contrast"] .wsp-app-fam-count,
body[data-theme="high-contrast"] .wsp-app-empty {
    color: #e5e7eb;
}

body[data-theme="high-contrast"] .wsp-app-fam-count {
    background: #1f2937;
}

/* ── wider screens ───────────────────────────────────────────────────────
   /app is built for a phone, but it is a real URL and someone will open it on
   a desktop. Cap the column rather than letting 151 rows stretch to 1900px. */
/* ⛔ ONE CONTENT WIDTH AND ONE GUTTER, OR IT IS THREE DIFFERENT COLUMNS.
   Measured at 1440 before this was fixed (owner reported it from the installed
   app on Windows):

       search input   680.0px @ left 380.0
       segmented ctl  680.0px @ left 406.6     <- .wsp-app-sort was uncapped
       family card    652.8px @ left 393.6     <- .wsp-app-main's own padding

   Two separate causes, both invisible in the source. (1) The cap listed
   `.wsp-app-seg`, but the control was later WRAPPED in `.wsp-app-sort`; capping
   a flex child inside a full-width parent centres it in the PARENT, not in the
   page, so it drifted right by half the label's width. Cap the WRAPPER.
   (2) `.wsp-app-main` carries `.85rem` of horizontal padding — correct on a
   phone, where it matches the bar's own padding — but the bar's children are
   capped INSIDE that padding while main's cap sat OUTSIDE it, so the cards came
   out 27.2px narrower than the search box above them.

   ⭐ The fix is to cap main at the content width PLUS the gutter and keep its
   padding, rather than capping it at the content width and dropping it. Both
   columns are then min(680, viewport - 1.7rem) at the same left edge — which
   holds at EVERY width, not only at the two that get asserted. Dropping the
   padding instead leaves a 3.6px mismatch between 700px and ~707px, where the
   viewport is narrower than the cap plus the gutter. */
@media (min-width: 700px) {
    .wsp-app-bar-row,
    .wsp-app-search,
    .wsp-app-sort { max-width: 680px; margin-left: auto; margin-right: auto; }

    .wsp-app-main {
        max-width: calc(680px + 1.7rem);   /* content + the .85rem gutters it keeps */
        margin-left: auto;
        margin-right: auto;
        padding-top: 1rem;
    }
}

/* Larger stage art once there is room for it to read as a preview. */
/* ⛔ BOTH BOXES, ALWAYS. The family art and the row art are the same size by
   design (see the alignment note above); growing only one of them here would
   re-create the exact misalignment this fixed, and only above 480px where it
   would be easy to miss. */
@media (min-width: 480px) {
    .wsp-app-fam-art,
    .wsp-app-tool-icon { width: 74px; height: 52px; }

    .wsp-app-tool { min-height: 70px; }
}
