/* The phone app carousel's stylesheet — the verbatim <style> block that
   lived inline in templates/partials/phone_app_carousel.html until
   2026-08-18, moved here so it caches instead of riding every page
   (~84KB of a ~1MB document). Only the server-conditional fragments
   stayed behind in the partial as a small inline <style>: the
   coloured-backgrounds wash rule (preference-gated — declared with the
   pref off it forms a custom-property cycle, see the comment there) and
   the E Ink theme overrides (user_brand is a server-side switch).
   Everything here is width-gated internally (body:not(.kik-pac) guards
   plus the 680px media blocks), so the <link> carries no media
   attribute and order relative to the inline overrides is what makes
   the ink rules win: the partial's <style> follows this link. */
/* Crossing back above 680px removes body.kik-pac but leaves the shell's
   furniture in the DOM (#kik-pac-titles is appended to #app-space and
   never reaped) — and every rule that sizes it lives behind body.kik-pac,
   so a stranded app-label svg rendered at natural size: a viewport-sized
   icon over the desktop dashboard. Off-mode, none of it may paint. */
body:not(.kik-pac) #kik-pac-titles,
body:not(.kik-pac) #kik-pac-empty { display: none !important; }
/* …and the same for the one-time hint, which is rendered by the DESK (see
   kik_phone_home.html) and so is in the document at every width. Everything
   that makes it a bubble — its box, its beak, even the display:none it is
   gated on — lives inside the phone breakpoint below, so above 680px the node
   had NO styling at all and rendered as a plain block of text with an × in the
   middle of the dashboard. What the bubble describes (press and hold the
   candy box to switch apps) is a phone gesture, so off the phone shell it does
   not exist. */
body:not(.kik-pac) .kik-pac-hint { display: none !important; }
/* Twice, on purpose. The rule above answers the CLASS being dropped; this one
   answers the WIDTH, so the bubble cannot appear on a desktop even if the
   shell class lingers (it is written by script, and the one thing this node
   must never do is turn up on a screen that has no candy box to press). */
@media (min-width: 681px) {
    .kik-pac-hint { display: none !important; }
}

@media (max-width: 680px) {
    /* THE PORTAL IS THE PHONE HOME AGAIN (Daniel, 2026-08-15): the apps grid
       and Today are portal furniture (#kik-phone-home, base.html), not
       app-space pages. The dashboard pane steps aside for it — same early
       rules base.html states before first paint, re-stated here so the pair
       cannot drift apart. */
    #portal #portal-content-pane { display: none !important; }
    #kik-phone-home { display: block; }

    /* #app-space is the carousel scroller for OPEN APPS ONLY. Its box is
       unchanged (absolute, below the header) — only its inner layout
       changes. At rest it is pointer-DEAD glass over the portal home; it
       takes the pointer only while an app is the foreground (state-on) or
       the switcher is up. */
    body.kik-pac #app-space {
        display: flex !important;
        flex-flow: row nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x mandatory;
        overscroll-behavior-x: contain;
        pointer-events: none;
        scrollbar-width: none;
    }
    body.kik-pac #app-space.state-on,
    body.kik-pac-switch #app-space {
        pointer-events: auto;
    }
    /* Going into (and out of) the switcher is eased, not cut: the stage
       dims as the cards step back, and brightens again on the way out —
       one clock for both directions (Daniel, 2026-08-15). */
    body.kik-pac #app-space {
        transition: background-color 0.4s ease;
    }
    body.kik-pac #app-space::-webkit-scrollbar { display: none; }

    /* THE CAROUSEL IS A MODE, NOT THE RESTING STATE (Daniel, 2026-08-15).
       At rest one app fills the screen and a horizontal drag belongs to that
       app's own content — the shell's pages only slide under a finger while
       the SWITCHER is up (long-press on the candy box, .kik-pac-switch on
       <body>). The gesture is taken away by TOUCH-ACTION on the pages, not
       by hiding the overflow (2026-09-12): a scroll timeline that comes into
       being while the row is overflow-x: hidden stays inactive on WebKit
       after the overflow opens, and the live carousel rides that timeline —
       so the row stays scrollable at rest and a page in front simply refuses
       the sideways pan. touch-action is read along the chain from the touch
       to the nearest scroller: an app's own horizontal scroller is nearer
       than the row and keeps its pan-x; content that does not scroll reaches
       the page, which allows the vertical pan and the pinch and nothing
       else. The scroller stays programmable either way (teleports, tile
       opens). The dot track goes with the gesture: a position indicator for
       a carousel you cannot swipe says nothing. */
    /* HIDDEN OVERFLOW AT REST AFTER ALL (Daniel, 2026-09-12: the carousel
       still took swipes inside a full-screen app). touch-action on the pages
       is read only as far as the nearest scroller, so a pan begun in the
       app's own list never consults it; and overscroll-behavior is consulted
       only on a box that can scroll in that axis, which the list cannot —
       iOS hands the sideways pan straight to the row either way. Only a row
       that cannot scroll refuses it. The wedge this used to cause — a scroll
       timeline born while the row was hidden — is met by building the
       tracks only while the carousel is live, where the row is always
       scrollable (buildSda). A home with nothing else open is hidden too:
       nothing to swipe to, no bar to show. */
    body.kik-pac:not(.kik-pac-switch) #app-space,
    body.kik-pac.kik-pac-desk-alone #app-space {
        overflow-x: hidden;
    }
    body.kik-pac:not(.kik-pac-switch) #app-space {
        /* NO SNAP AT REST (Daniel, 2026-08-15): the shell owns the resting
           position outright (teleports). With snapping live, a loading
           app's relayout let the browser re-snap toward its stale
           remembered target and the observer slid it back — the "quick
           small shift left/right, then the app falls into place" on every
           open. Snapping belongs to the switcher, where a finger drives. */
        scroll-snap-type: none;
    }
    body.kik-pac:not(.kik-pac-switch) #kik-pac-dots {
        display: none;
        opacity: 0;
    }
    /* THE TRACK TAKES THE POINTER ONLY IN THE SWITCHER PROPER (Daniel,
       2026-09-20: invisible carousel controls sat over the dock's icons and
       ate their taps). Two states leave it painted at opacity 0 but live:
       the live carousel at the FRONT — home at slide one with apps open,
       where the mode's class is on and the track is faded but the dock row
       is the thing on screen — and the second after the mode ends, where
       the docked rule's `display … allow-discrete` holds the display flip
       back for the fade. Its scrub hit area (::before, 20px up and 32px
       out) reaches well into the icons beside its centre. Off the pointer
       in both; the switcher's own rule below puts it back. */
    body.kik-pac:not(.kik-pac-switch) #kik-pac-dots,
    body.kik-pac.kik-pac-switch:has(.kik-pac-front) #kik-pac-dots {
        pointer-events: none;
    }

    /* Non-page children (tabs bar, quick nav) leave the flex flow. The
       empty-switcher line, the titles layer and the stage veil are exempt
       — each has its own show rule below. */
    body.kik-pac #app-space > :not(.application-body):not(#kik-pac-empty):not(#kik-pac-titles):not(#kik-pac-stage) {
        display: none !important;
    }

    /* Loading feedback belongs to the LOADING PAGE, not the shell: the stock
       #app-space.injecting spinner sits on the scroller itself, so it stayed
       in view when the user swiped away from a loading app. Kill it (and the
       all-pages dim) in carousel mode; the loading body carries its own
       centred spinner instead, which travels with its page. */
    body.kik-pac #app-space.injecting::before { display: none !important; }
    body.kik-pac #app-space.injecting > * { opacity: 1 !important; }
    /* Dim the CONTENT, never the page. A page's own wash is what makes it
       opaque, so dropping the whole body to 0.55 let everything behind
       #app-space through — and since the dock is a translucent surface, the
       bleed showed through that too. On a phone that reads as a grey veil
       washing over the entire screen (and the dock's icons flickering behind
       it) every time an app loads; caught on a screen recording, 2026-08-12.
       Dimming the children leaves the wash doing its job. */
    body.kik-pac #app-space > .application-body.injecting { opacity: 1 !important; }
    body.kik-pac #app-space > .application-body.injecting > * { opacity: 0.55; }
    body.kik-pac #app-space > .application-body.injecting::before {
        content: var(--glyph-spinner);
        position: absolute;
        top: 50%;
        left: 50%;
        width: 50px;
        height: 50px;
        margin: -25px 0 0 -25px;
        z-index: 5;
        font: var(--pat-throbber-size)/var(--pat-throbber-size) fontello;
        text-align: center;
        color: var(--pat-throbber-colour, black);
        animation: spin 2s infinite linear;
    }

    /* THE wash: each page is one colour — 5% of its app's colour over
       white (launcher share = brand accent) — via a --kik-pac-wash var
       stamped STATICALLY per page (stampWash in the JS, once per open).
       The swipe transition is spatial: the colours travel with the sliding
       pages. Deliberately NOT a cross-fade — v2 animated this var on
       #app-space per scroll frame, and animating an inherited custom
       property forces a style recalc across every open app's whole DOM
       (Messages alone is thousands of nodes) on the main thread every
       frame; swiping janked hard. Static washes cost zero per frame.
       Page surfaces are OPAQUE (they paint the wash, they are not glass):
       apps stack their phone panes absolutely and rely on pane backgrounds
       to hide the pane underneath — full transparency let a task detail
       bleed through the Reminders list.

       The launcher share of that recipe is --kik-app-wash (:root, in
       cloud-dashboard.css), because the bento board is painted in it too:
       the desk and this carousel are the same home seen two ways, and two
       literals of one recipe drift the moment either is retuned. The JS
       keeps computing the per-app variant (washOf) — those are one colour
       per page, not this one. */
    body.kik-pac #app-space.state-on {
        background-color: var(--kik-app-wash);
    }
    /* The home keeps the same faint brand wash the launcher page wore — it
       is the instance's accent, painted on the portal now that the portal
       is the home surface. */
    body.kik-pac #portal {
        background-color: var(--kik-app-wash);
    }
    /* THE SWITCHER'S BACKDROP: the application background — the BEIGE — a bit
       darker (Daniel, 2026-08-15). The recipe now lives at :root as
       --kik-sw-stage (cloud-dashboard.css), because the desk's bento stands on
       the same ground above the phone breakpoint and never carries this
       class. The transition on the base rule eases it both ways. */
    /* …painted as a VEIL over the wash since 2026-09-12: #kik-pac-stage,
       inside #app-space (the JS seats it there, under the titles and the
       cards), its opacity the row's step back — 0 on the home, 1 one slot
       in — on the same scroll timeline as everything else, so the ground
       darkens with the swipe and lightens on the way home. Always in the
       tree, so leaving to an app fades it as the colour used to fade. */
    #kik-pac-stage { display: none; }
    body.kik-pac #kik-pac-stage {
        display: block;
        position: fixed;
        inset: 0;
        background: var(--kik-sw-stage);
        opacity: 0;
        z-index: 0;
        pointer-events: none;
        transition: opacity 0.4s ease;
    }
    body.kik-pac.kik-pac-switch #kik-pac-stage {
        opacity: 1;
    }

    /* Every OPEN app body is a full-screen page — but only while an app IS
       the foreground (space state-on) or the switcher is up. At rest with
       no active app the open bodies stay hidden and the portal home shows
       through. */
    /* Four classes each, both !important: the hide outranks the generic
       three-class show below it, and the switcher rule — same weight, later
       in the sheet — outranks the hide while the mode is up. */
    body.kik-pac #app-space:not(.state-on) > .application-body.state-on {
        display: none !important;
    }
    body.kik-pac #app-space > .application-body.state-on {
        display: block !important;
        position: relative;
        flex: 0 0 100%;
        width: 100%;
        min-width: 100%;
        height: 100%;
        overflow: hidden;
        /* THE PAGE ANCHORS ITS OWN FIXED CHROME (Daniel, 2026-08-16: the
           toolbars wobble). The house main toolbar is `position: fixed`
           now — windowed AND normal mode, windowed-mode.css — and that
           rule anchors it by putting `contain: layout` on the window. A
           carousel page is no window and carries no transform at rest (a
           transformed page would be a containing block for its app's own
           fixed chrome, which the mode note further down refuses), so
           every open app's bar fell back to the VIEWPORT: measured on a
           390px phone, the off-screen Messages page sat at left −390 with
           its toolbar still painting at 0. On screen that reads as the bar
           sliding inside its own card while the card swipes, then snapping
           home the moment the switcher's scale() hands it a containing
           block after all — a wobble on every settle.
           Layout containment is the same lever windowed mode already
           pulls, and it costs no transform: the page becomes the
           containing block, the bar rides with its card, and a card that
           is off screen takes its toolbar with it. */
        contain: layout;
        scroll-snap-align: center;
        scroll-snap-stop: always;
        /* The page root paints its own wash opaquely — gaps between panes
           show the page's tint, never a neighbour's or the launcher's.
           It paints THROUGH --application-body-background-colour (defaulted
           to the wash right below) rather than off the wash directly, so an
           app that legitimately re-points that variable still lands on the
           page root here. Mail is the one that does: an open message whose
           HTML declares its own light page background sets the variable on
           #application-body-mail so the chrome blends into the message
           instead of framing it with a hard edge (wiki → Apps/Mail, "the
           three layers"). Painting the raw wash instead left the phone with
           a white message card marooned on a blue-washed body.

           TWO LAYERS, deliberately (Daniel, 2026-08-15: under load a body
           sometimes painted with NO background at all). The wash rides on
           background-image; background-color is a LITERAL floor. The var
           chain is only ever a custom property away from guaranteed-invalid
           — a ::element swap drops the inline wash stamp until the observer
           restamps it — and an invalid var takes its whole declaration with
           it. A poisoned image leaves the white floor; a transparent page
           over the switcher's dark stage cannot happen. */
        /* The floor stays one token deep, never a stamped chain: the theme's
           opaque card tone (--colour-base-light, white on every light theme)
           is declared at :root and cannot be dropped by a ::element swap the
           way the inline wash stamp can — so the guarantee the literal #fff
           gave (an opaque pane, always) survives, and a dark theme's pane is
           dark instead of blinding. */
        background-color: var(--colour-base-light, #fff);
        background-image: linear-gradient(
            var(--application-body-background-colour, var(--kik-pac-wash, var(--colour-base-light, white))),
            var(--application-body-background-colour, var(--kik-pac-wash, var(--colour-base-light, white))));
        --sidebar-background-colour: var(--kik-pac-wash, var(--colour-base-light, white));
    }
    /* Sidebar and document body both paint the SAME wash — identical colour
       to the page root, but each opaque, because stacked panes must hide
       what is underneath them. Per-app rules that tint deeper,
       content-level elements are untouched. */
    body.kik-pac #app-space > .application-body.state-on .sidebar,
    body.kik-pac #app-space > .application-body.state-on .sidebar-content {
        background: var(--kik-pac-wash, var(--colour-base-light, white));
    }
    /* The document body follows the app-body colour, not the raw wash — it
       is the surface the open message sits on, and on desktop it is simply
       transparent over .application-body's own
       var(--application-body-background-colour). Same source of truth here,
       just painted rather than inherited, because the pane must stay
       opaque. */
    body.kik-pac #app-space > .application-body.state-on .document-body {
        background: var(--application-body-background-colour, var(--kik-pac-wash, var(--colour-base-light, white)));
    }

    /* Compact phone sidebars: the desktop airiness reads as emptiness on a
       390px screen. Tighten the three spacings that add up to it — the gap
       between the nav pills (stock 18px), the gap between wells (stock
       ~19px), and the row height inside a well's .pat-menu-list list (stock 8px
       vertical link padding). */
    body.kik-pac #app-space > .application-body.state-on .sidebar-content .sidebar-extras {
        gap: 8px;
        margin-bottom: 10px;
    }
    body.kik-pac #app-space > .application-body.state-on .sidebar-content .pat-well {
        margin-bottom: 10px;
    }
    body.kik-pac #app-space > .application-body.state-on .sidebar-content .pat-menu-list > li > a {
        padding-top: 5px;
        padding-bottom: 5px;
    }
    body.kik-pac #app-space > .application-body:not(.state-on) {
        display: none !important;
    }

    /* THE PHONE HOME (partials/kik_phone_home.html): Today as a collapsible
       well above the full apps grid. It renders inside the DASHBOARD APP on
       One/Pro (2026-08-16) — the desk's own body scrolls it, so it needs no
       scroller of its own, and the page inset that #portal-content used to
       give it now comes from apps/dashboard/styling/all.css. The grid is
       styled ONLY by .kik-apps-grid (apps-grid.css). The bottom padding
       clears the dock. */
    /* ONE inset all round (Daniel, 2026-08-15): the pane's own paddings
       were asymmetric on a phone — the home reads off the platform's
       small-screen padding on every side instead. The dock clearance rides
       on the home block's bottom, scroll room rather than visual inset. */
    /* :has(#kik-phone-home) is for WEIGHT, not meaning (the block is always
       there on these faces): the bento pager's own
       `#portal #portal-content:has(.kik-cloud-dashboard.is-paged)` rule
       zeroes padding-bottom — the hidden dashboard pane still carries that
       markup — and out-specifies a plain class+id chain. */
    /* Five pixels less at either side than the platform gutter (Daniel,
       2026-09-12): the tiles read better a touch wider on the phone. */
    body.kik-pac #portal #portal-content:has(#kik-phone-home) {
        padding: var(--small-screen-padding) calc(var(--small-screen-padding) - 5px);
    }
    body.kik-pac #kik-phone-home {
        display: block;
        padding: 0 0 calc(var(--kik-dock-clearance, 85px) + 1rem);
        box-sizing: border-box;
        /* A little less air at the sides (Daniel, 2026-09-12): the platform's
           15px phone inset, less this, at left and right — taken here as a
           negative margin rather than from the pane's padding, so the desk's
           own chrome keeps its inset. The Today widget's top takes the same
           amount off (below), to stay as far from the top as from the sides. */
        --kik-home-inset-less: 5px;
        margin-inline: calc(-1 * var(--kik-home-inset-less));
    }
    /* THE TODAY WIDGET IS CUT FROM THE GRID'S OWN CLOTH (Daniel, 2026-09-12):
       four icons wide, one icon tall, one icon-gap above the icons, and as
       far from the top of the screen as it is from the sides. The grid is
       four square cells across the home's width (C = 25% of it), with the
       icon 70% of its cell and centred — so the icons are 0.3C apart and
       the first row's icons start 0.15C in from the grid's edge and 0.15C
       down from its top. Everything here is a percentage of the home's
       width, which is what C is: width 3.7C, 0.15C of side inset (the same
       0.15C the icons keep), 0.15C above (the page's padding is already
       there, so top and sides both come to padding + 0.15C), 0.15C below
       (the 0.3C gap less the 0.15C the icon already sits down its cell),
       and — closed — a height of 0.7C through the aspect ratio, since a
       percentage height has nothing to resolve against. It rides on the
       BOARD that wraps the card (cloud-dashboard.css gives the card its
       chrome off that wrapper), not on the card itself — a board is a
       grid, and a margin on its only child would sit inside it. */
    body.kik-pac #kik-phone-home .kik-phone-today-board {
        width: 92.5%;
        margin: calc(3.75% - var(--kik-home-inset-less, 0px)) auto 3.75%;
        padding: 0;
        gap: 0;
        grid-template-columns: minmax(0, 1fr);
    }
    body.kik-pac #kik-phone-home .kik-phone-today-board:has(#portlet-phone-today.closed) {
        aspect-ratio: 37 / 7;
    }
    /* …and the closed card fills that box with its strip centred: the
       well's vertical padding goes (the box is an icon tall, the toggle
       row is most of that), and the group sits in the middle. */
    body.kik-pac #kik-phone-home #portlet-phone-today.closed {
        --pat-well-padding-top: 0px;
        --pat-well-padding-bottom: 0px;
        height: 100%;
        box-sizing: border-box;
        display: grid;
        align-content: center;
    }

    /* THE CLOSED CARD IS ONE LINE FROM THE FIRST PAINT (Daniel, 2026-09-11:
       the cold start "bounced up and down"). pat-collapsible hides a closed
       card's body when it INITIALISES, which on a phone is a good half-second
       after the page has painted — and until then the body stood open under
       the strip, empty but for its lazy anchor, so the card drew two lines
       tall and the whole apps grid sat 65px lower than where it was about to
       be. The stylesheet says what the pattern is going to say, so nothing
       moves when it does: the closed body is hidden by its class, and the
       title row keeps the toggle's height whether the strip has arrived or
       not. Not gated on body.kik-pac — that class is the same JS boot this
       has to be true before. Opening removes the class (and slideDown writes
       an inline display anyway), so the pattern's own motion is untouched. */
    #kik-phone-home #portlet-phone-today.closed > .panel-content {
        display: none;
    }
    #kik-phone-home #portlet-phone-today.closed > .well-title-group {
        min-height: var(--collapsible-button-height, 31px);
    }
    /* …AND IT FADES IN ONCE IT HAS SOMETHING TO SAY. The strip is a fetch
       away, and an empty box with a chevron that fills a moment later is the
       same restlessness as the jump, just quieter. So the card is invisible
       until its summary has landed — the fragment always renders the event
       cell, planned or "nothing planned" — and eases in then. The height was
       there from the start (above), so the grid does not move for this. A
       card stored OPEN loses .closed on init and its body then shows; the
       summary still arrives through the same fetch, so the fade holds. */
    #kik-phone-home #portlet-phone-today {
        opacity: 0;
    }
    #kik-phone-home #portlet-phone-today:has(.kik-today-summary-event) {
        opacity: 1;
        transition: opacity calc(var(--base-animation-duration, 0.3s) * 1.5) ease;
    }
    /* THE APP FADES IN LAST (Daniel, 2026-09-11). The launch screen is the
       wash and the dock, and that is exactly what the first paint should be
       — so the pages stay at nothing until the shell has booted (body.kik-pac
       is the carousel's own "I am running", set synchronously in apply())
       and then ease in over the splash's own picture. The bar is outside
       #app-space and is never faded: it is the half the splash already drew.
       The class follows the same 680px query as this block, so a window that
       is not a phone never starts hidden; the one way to a blank phone is the
       carousel script failing to run at all, which takes the launcher with it
       regardless. */
    #app-space {
        opacity: 0;
    }
    body.kik-pac #app-space {
        opacity: 1;
        transition: opacity calc(var(--base-animation-duration, 0.3s) * 1.5) ease;
    }
    @media (prefers-reduced-motion: reduce) {
        body.kik-pac #app-space,
        #kik-phone-home #portlet-phone-today:has(.kik-today-summary-event) {
            transition: none;
        }
    }

    /* THE CLOSED CARD IS A SUMMARY STRIP (Daniel, 2026-09-03): the weather
       for the coming hour or two, then the next appointment, then the toggle
       on the right (the date dial that used to lead it went on 2026-09-09,
       with the bento card's) — one line that says what
       the day holds without opening it. The strip is the card's summary slot
       (cloud_dashboard_card.html, .kik-cloud-card-summary), filled by the
       fragment; open, it goes away and the card is the desktop card again:
       its title and its day nav come back.

       What goes with it while shut: the title (the strip IS the title now),
       and the whole day-nav group — arrows and '+'. A closed card has no day
       to walk (Daniel, 2026-08-16), and the '+' gives way to the room the
       appointment needs. Displayed away, not faded: the strip has to take
       the width they held. The title group is a grid; the closed card gives
       it a column for the strip and none for the page head, which holds
       nothing but the hidden h1 — an empty auto track would still keep its
       gap in front of the strip. */
    body.kik-pac #portlet-phone-today .kik-cloud-card-summary {
        display: none;
    }
    /* The third column is the toggle's, sized by the same token the well
       gives its own toggle column (well.css): the toolbar's intrinsic width
       is ZERO (its sections carry percentage widths), so an `auto` track
       collapses and the button overhangs the strip, which then truncates
       under it instead of before it. */
    /* And it is a STRIP (Daniel, 2026-09-03: "too tall when collapsed"):
       no minimum height of its own — the row is as tall as the toggle. */
    body.kik-pac #portlet-phone-today.closed .well-title-group {
        grid-template-columns: minmax(0, 1fr) var(--collapsible-button-height, 31px);
        align-items: center;
    }
    body.kik-pac #portlet-phone-today.closed .quaive-page-head,
    body.kik-pac #portlet-phone-today.closed .toolbar-day-nav-controls {
        display: none;
    }
    body.kik-pac #portlet-phone-today.closed .kik-cloud-card-summary {
        display: flex;
        align-items: center;
        gap: 12px;
        min-width: 0;
        color: var(--colour-ink, inherit);
        line-height: 1.2;
    }

    /* The weather cell: the condition glyph (partials/weather_icon.html —
       sized by font-size) with the temperature tucked beside it, kept as one
       unbreakable unit. It borrows the weather library's accents, so a sun
       is a sun and rain is rain here as everywhere else (weather-portlet.css
       scopes those rules to .kik-cloud-card-summary too). A thin rule after
       it separates the sky from the day. */
    body.kik-pac #portlet-phone-today .kik-today-summary-weather {
        display: inline-flex;
        align-items: center;
        gap: 5px;
        flex: 0 0 auto;
        font-size: 1.45rem;
        padding-right: 12px;
        border-right: 1px solid color-mix(in srgb, currentColor 18%, transparent);
    }
    body.kik-pac #portlet-phone-today .kik-today-summary-temp {
        font-size: 1rem;
        font-weight: 600;
        font-variant-numeric: tabular-nums;
        letter-spacing: -0.01em;
    }

    /* The appointment: its time first, in the card's accent (the same ink
       the day's title wears), then the title in the running text, one line,
       ellipsised — a long meeting name gives way before the toggle does. A
       running event says "now" instead of a time; an event on another day
       puts its weekday in front. */
    body.kik-pac #portlet-phone-today .kik-today-summary-event {
        display: flex;
        align-items: baseline;
        gap: 8px;
        min-width: 0;
        flex: 1 1 auto;
        font-size: 1.05rem;
    }
    body.kik-pac #portlet-phone-today .kik-today-summary-time {
        flex: 0 0 auto;
        font-weight: 600;
        font-variant-numeric: tabular-nums;
        color: var(--kik-cloud-card-title-colour, var(--app-colour));
        white-space: nowrap;
    }
    body.kik-pac #portlet-phone-today .kik-today-summary-time.is-running {
        text-transform: lowercase;
    }
    body.kik-pac #portlet-phone-today .kik-today-summary-day {
        font-weight: 500;
        opacity: 0.75;
    }
    body.kik-pac #portlet-phone-today .kik-today-summary-title {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    body.kik-pac #portlet-phone-today .kik-today-summary-empty {
        opacity: 0.6;
        font-style: italic;
    }

    /* The title is the card's open/close control, so it takes a hit area
       bigger than its own ink: an ::after stretches it into the bar's
       padding — up, down and to the leading edge, never to the right, where
       the toolbar's own buttons live and would be swallowed. (Closed, the
       whole title group opens the card — the JS's rule — so the summary
       strip needs no reach of its own.) */
    body.kik-pac #portlet-phone-today .quaive-page-head {
        position: relative;
    }
    body.kik-pac #portlet-phone-today .quaive-page-head::after {
        content: "";
        position: absolute;
        top: -14px;
        bottom: -14px;
        left: -14px;
        right: 0;
        border-radius: inherit;
    }

    /* (The tap-feedback pressed state on app tiles lives in
       libraries/kikaron/apps-grid/apps-grid.css — it styles grid tiles, and
       everything that styles .kik-apps-grid lives there. The launching pulse
       that used to sit beside it is gone, JS and all.) */

    /* "Kikaron" home tab: swirl tile icon + brand name, styled like an
       active app tab, inserted into the labels row by the JS. Visibility is
       purely GEOMETRIC, exactly like the app labels: the always-rendered
       base rule displays it and paintLabelsAt() drives opacity/
       pointer-events from the centred page — no class-based reveal that
       could disagree with the geometry during transient states. The icon
       renders through the SAME pipeline as app icons (.kal-icon tile,
       --kal-icon-bg + inline white-swirl svg) so sizes match by
       construction; on desktop the tab simply never gets revealed. */
    #kik-pac-home {
        --kal-icon-bg: #F37021;
        --kal-icon-fg: #fff;
        --kal-border: #F37021;
        --kal-text: #F37021;
    }
    /* Logo flavour: the tab IS the wordmark. The .kal-icon box is a 33px
       square with contain:size and overflow:hidden (global-nav-side.css) —
       right for an app glyph, fatal for a mark that is wider than it is
       tall, which would be clipped to a third of itself. Let the box take
       the mark's own width, and drop the orange plate and border the swirl
       tile wears: a logo brings its own colour. */
    #kik-pac-home.kik-pac-home--logo {
        --kal-icon-bg: transparent;
        --kal-border: transparent;
        /* And no wash under the pointer either. Every other tab in the row
           takes the grey on hover / while its panel is open (kik_app_label.html
           reads this token for both), but those are tiles in a strip; this one
           is a wordmark sitting on the header, and a grey rectangle behind it
           reads as a box drawn around the brand. Set as the TOKEN rather than
           by overriding background-color, so it covers the open-menu state the
           same way. */
        --kal-hover-bg: transparent;
    }
    #kik-pac-home.kik-pac-home--logo .kal-icon {
        width: auto;
        min-width: 0;
        flex: 0 0 auto;
        contain: none;
        overflow: visible;
        background: none;
        /* Nudged down onto the row's optical centre. The mark's ink sits high
           in its own box (the wordmark's baseline, not its bounding box, is
           what the eye lines up), so a box centred by the flex row reads a
           couple of pixels high. !important answers the `padding: 0 !important`
           the swirl tile carries below — same element, and that one has to
           shout because the shared tab-icon rules do. */
        padding-top: 4px !important;
    }
    #kik-pac-home .kik-pac-home-logo {
        display: block;
        height: 38px;
        width: auto;
        max-width: 42vw;
        object-fit: contain;
    }

    /* The swirl fills the tile: no glyph padding (the app icons keep
       theirs — the swirl's own curves provide the breathing room). */
    #kik-pac-home .kal-icon { padding: 0 !important; }
    #kik-pac-home .kal-name { pointer-events: none; }

    /* App-label hand-over animation: on an app switch the incoming label
       slides in ~50px and fades, the outgoing one slides out and fades AT
       THE SAME TIME, both in the swipe's travel direction (JS stamps the
       -l/-r classes from the pages' real geometry, so RTL just works). The
       outgoing label is forced visible (its :has() reveal rule stopped
       matching the instant the classes flipped) and lifted out of flow so
       the two labels cross-fade in place. */
    /* EVERY label is always rendered, stacked absolute at the same spot and
       vertically centred — visibility is purely transform + opacity +
       pointer-events (JS-driven from the live scroll fraction), so nothing
       ever re-flows or jumps vertically during the hand-over. The keyframes
       (used for out-of-band jumps only) all carry the same translateY(-50%)
       so the vertical anchor never changes mid-animation. */
    /* overflow visible: the base #kik-app-labels carries overflow:hidden
       (it clips the flex row on desktop), which cut the slide animation at
       the container's left edge — in carousel mode the labels are absolute
       overlays, so nothing needs that clip and only the viewport should
       cut the motion. */
    body.kik-pac #global-header #kik-app-labels {
        height: 100%;
        overflow: visible;
    }
    body.kik-pac #global-header .kik-app-labels-row {
        position: relative;
        display: block;
        height: 100%;
        overflow: visible !important;  /* a 2-id base rule pins hidden */
    }
    body.kik-pac #global-header kik-app-label {
        display: inline-flex !important;
        position: absolute;
        inset-inline-start: 0;
        top: 50%;
        transform: translate(0, -50%);
        opacity: 0;
        pointer-events: none;
        margin: 0;
        max-width: calc(100vw - 150px);
    }
    /* Glide is CONDITIONAL (kal-glide, stamped by the painter): during a
       smooth swipe the labels are painted frame-locked with NO transition —
       an always-on transition chased a moving target and looked shaky. The
       class goes on only when frames were starved (heavy app boot) or on a
       settle/jump, so late paints glide instead of teleporting. */
    body.kik-pac #global-header kik-app-label.kal-glide {
        transition: transform 0.15s linear, opacity 0.15s linear;
    }
    /* Every label carries the ACTIVE look permanently on the carousel —
       app-colour icon tile, white glyph, coloured name — so an incoming
       label already wears its colours while it slides in, not only after
       the settle flips the :has() active rules. (On the carousel a label
       is only ever visible as the active/incoming app, so the idle grey
       look has no moment to exist.) Values mirror kik_app_label.html's
       active rule; each label's inline --app-colour scopes them per app.
       The home tab is excluded — its icon is an <img> tile. */
    body.kik-pac #global-header kik-app-label:not(#kik-pac-home) {
        /* Inset for the tinted pill the tab wears on phones
           (kik_app_label.html): with the label's own padding at 0 the icon tile
           sat flush against the wash's edge. The home tab is excluded here as
           everywhere in this block, and 5 between the icon, the name and the ×
           — a shade under the inset, so the pill's own edge stays the widest
           space in it and the three parts read as one group. */
        padding: 6px;
        gap: 5px;
        --kal-icon-bg: var(--kal-icon-bg-active, var(--app-tile-icon-colour, var(--app-colour)));
        --kal-icon-fg: var(--kal-icon-fg-active, #fff);
        --kal-icon-scale: var(--kal-icon-scale-active, 0.8);
        --kal-border: var(--app-colour);
        --kal-text: var(--app-colour);
    }
    @keyframes kik-pac-kal-in-l { from { transform: translate(50px, -50%); opacity: 0; } to { transform: translate(0, -50%); opacity: 1; } }
    @keyframes kik-pac-kal-in-r { from { transform: translate(-50px, -50%); opacity: 0; } to { transform: translate(0, -50%); opacity: 1; } }
    @keyframes kik-pac-kal-out-l { from { transform: translate(0, -50%); opacity: 1; } to { transform: translate(-50px, -50%); opacity: 0; } }
    @keyframes kik-pac-kal-out-r { from { transform: translate(0, -50%); opacity: 1; } to { transform: translate(50px, -50%); opacity: 0; } }
    body.kik-pac #global-header kik-app-label.kal-anim-in-l { animation: kik-pac-kal-in-l 0.28s ease both; }
    body.kik-pac #global-header kik-app-label.kal-anim-in-r { animation: kik-pac-kal-in-r 0.28s ease both; }
    body.kik-pac #global-header kik-app-label.kal-anim-out-l { animation: kik-pac-kal-out-l 0.28s ease both; }
    body.kik-pac #global-header kik-app-label.kal-anim-out-r { animation: kik-pac-kal-out-r 0.28s ease both; }

    /* The hub list is NOT hidden from the personal menu here any more. It
       used to be, because the phone carousel kept its own copy in the home
       tab's menu and showing both listed every hub twice. The home tab is a
       plain link to the product's site now, so that copy is gone and the
       avatar is the only door to the hopper — on the phone as everywhere
       else. Hiding it here would leave a hub member with no way across. */

    /* The home tab's menu inherits the personal-menu phone frame from assets
       tooltip.css, which hugs the RIGHT edge (inset 50px left / 15px right —
       built for the avatar menu). The home tab sits at the LEFT edge, so
       mirror the frame to hug the left instead. Scoped via the menu's own
       ul.kik-home-menu so the avatar menu keeps its right-hugging frame. */
    body.kik-pac .tooltip-container.personal-menu:has(ul.kik-home-menu),
    body.kik-pac .context-menu-popover.personal-menu:has(ul.kik-home-menu) {
        inset: calc(var(--global-header-height) + 15px) 50px auto 15px !important;
    }

    /* Candy box while the app grid (launcher) is the active page: the nine
       circles render SOLID — Kikaron orange, or black on the E Ink themes
       (server-side brand switch; the rail icon is stroke-only otherwise). */
    body.kik-pac:has(#app-space:not(.state-on)) #compact-candy-box {
        color: #F37021;  /* ink themes override to #000 — inline block in the partial */
    }
    body.kik-pac:has(#app-space:not(.state-on)) #compact-candy-box svg circle {
        /* !important: an icon-normalisation rule elsewhere pins svg fills
           with !important — colour alone applied but the fill stayed none. */
        fill: currentColor !important;
        stroke: currentColor !important;
    }

    /* Mobile tabs DO carry their closing × (kik_app_label.html reveals it at
       ≤680px). It used to be hidden here, on the reasoning that closing lived
       in the app-label menu — but a tap anywhere on the label opens that menu,
       so with one tab on screen the × is the only one-tap way out of an app,
       and the menu is two. The delegation click handler below exempts it so
       the tap reaches its own pat-switch instead of being turned into a menu
       open. */
    /* Icons INHERIT their label's painted pointer-events (paintLabelsAt sets
       auto on the visible label only). A flat `auto` here let every HIDDEN
       stacked label's icon swallow taps — the topmost invisible icon opened
       ITS app menu instead of the visible tab's ("home tab shows the old
       menu" bug). */
    body.kik-pac #global-header kik-app-label .kal-icon {
        pointer-events: inherit;
    }
    /* The × inherits for exactly the same reason, and so does its invisible
       hit extender. In carousel mode every label is stacked at the same
       inset-inline-start with only opacity telling them apart, so a flat
       `auto` — which is what the ≤680px reveal gives it (kik_app_label.html) —
       would let a HIDDEN tab's × sit over the visible one and close the wrong
       app. The ::after needs saying separately: pointer-events:none on an
       ancestor does not stop a descendant that sets `auto` from being hit, and
       the component sets exactly that on the extender. */
    body.kik-pac #global-header kik-app-label .kal-close,
    body.kik-pac #global-header kik-app-label .kal-close::after {
        pointer-events: inherit;
    }

    /* The rail: a floating translucent pill of balls, one per OPEN app —
       same placement/feel as the dashboard pager's dots. Viewport-fixed so
       in-app scrolling never moves it. */
    /* How long the track takes to retreat, and the label's mask ramp with it. */
    body.kik-pac { --kik-pac-rest-fade: 1s; }

    #kik-pac-dots {
        position: fixed;
        left: 50%;
        transform: translateX(-50%);
        /* One offset, stated once: the rail rides in the dock strip, above
           the mobile bottom bar (global-nav-side.css: 64px tall, 10px off the
           edge). It used to be a JS-written variable that MEASURED whatever
           composer was on screen; that is gone (2026-08-12) — a focused
           composer now takes the strip and the whole dock steps aside for it
           in CSS, below. */
        --kik-pac-rail-bottom: var(--kik-dock-clearance,
                                   calc(82px + env(safe-area-inset-bottom, 0px)));
        bottom: var(--kik-pac-rail-bottom);
        z-index: 40;
        display: flex;
        align-items: center;
        gap: 9px;
        padding: 7px 12px;
        border-radius: 999px;
        background: rgba(255, 255, 255, 0.72);
        backdrop-filter: blur(6px);
        -webkit-backdrop-filter: blur(6px);
        box-shadow: 0 1px 6px rgba(0, 0, 0, 0.18);
        touch-action: none;
        /* bottom is transitioned too: swiping onto (or off) a composer page
           glides the rail up/down instead of teleporting it. */
        transition: transform 0.35s ease, opacity 0.35s ease,
                    bottom 0.2s ease-out;
    }
    /* Auto-hide: a few seconds after an app is in the foreground the rail
       slides down off-screen (JS arms/clears the timer; any swipe brings it
       back). Keeps the base centring transform. The travel is expressed in
       terms of the SAME offset variable, so a rail lifted above a composer
       still slides all the way off the screen edge. */
    #kik-pac-dots.kik-pac-dots-away {
        transform: translateX(-50%)
            translateY(calc(100% + var(--kik-pac-rail-bottom)));
        opacity: 0;
        pointer-events: none;
    }
    #kik-pac-dots[hidden] { display: none; }

    /* A FOCUSED COMPOSER OWNS THE DOCK STRIP. Typing is when the bottom of the
       screen is worth most (and, on a phone, when the keyboard has taken the
       rest of it), so the rail slides away for as long as the composer holds
       focus — the same travel and easing as the scroll auto-hide above, minus
       its timers. `:has()` on <body> because the composer lives inside an app
       page and the rail is a sibling of #app-space; there is no ancestor to
       hang a class on, and nothing in JS may touch this any more. */
    body:has(.kik-chat-composer:focus-within) #kik-pac-dots:not(.kik-pac-dots--docked) {
        transform: translateX(-50%)
            translateY(calc(100% + var(--kik-pac-rail-bottom)));
        opacity: 0;
        pointer-events: none;
    }

    /* Docked in the phone bottom bar (One/Pro — dockNav below moves it there
       when #global-nav-side exists). The bar already IS the floating pill, so
       the rail sheds its own pill entirely — no wash, no blur, no shadow, no
       hairline — and becomes just the balls, centred in the bar with the candy
       box sitting at the right end (global-nav-side.css).
       Centred against the BAR, not against the space left over beside the
       candy box: absolutely positioned at the bar's midpoint, so the balls
       stay dead centre however wide that control is. `bottom: auto` with
       !important so top+bottom together can never stretch it — that used to
       matter because a JS painter wrote an inline --kik-pac-rail-bottom here;
       the painter is gone (2026-08-12), the belt-and-braces stays.

       Neither hide applies while docked: the bar owns visibility, so it is
       the bar that slides away on scroll-down, and the bar that steps down
       for a focused composer — the rail rides along inside it either way. */
    #kik-pac-dots.kik-pac-dots--docked {
        position: absolute;
        left: 50%;
        /* Half the bar's CONTROL BAND, not half the bar. The bar's
           height grows by the safe area and pads that space below its
           controls, so `top: 50%` centres on the padding box and drops
           everything absolutely-seated in it by half the inset — on a
           notched phone the label sat visibly lower than the candy box
           (a flex child, laid out in the content box) and the media chip
           (seated from the bottom). Measure from the top edge instead,
           which the padding never moves. */
        top: calc(var(--workspace-tabs-bar-height, var(--gns-mobile-height)) / 2);
        bottom: auto !important;
        transform: translate(-50%, -50%);
        background: none;
        box-shadow: none;
        border: 0;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
    /* The auto-hide never arms while docked (armRailHide bails), but the class
       can survive the move — keep the seating transform whatever happens. */
    #kik-pac-dots.kik-pac-dots--docked.kik-pac-dots-away {
        transform: translate(-50%, -50%);
        opacity: 1;
        pointer-events: auto;
    }

    /* (The resting mark — the wordmark that crossfaded with the track — and
       the active-app label that later took its seat both went on 2026-09-20:
       the bar's left end is the dock's app row now, phone_dock_apps.html and
       libraries/kikaron/phone-dock.) */

    /* The track at rest: a second after the carousel stops, with no finger on
       it, it fades out — it says where you are, and once you have stopped
       moving you know. And once faded it is GONE to the pointer too (Daniel,
       2026-08-12): an invisible strip that still ate taps sat exactly where
       the active-app label's tap target ends, so touches aimed at the bar hit
       nothing. Only the carousel moving again brings it back (scroll →
       showRail); until then the bar's controls own the whole strip. */
    #kik-pac-dots.kik-pac-dots--docked {
        /* A LONG fade — a second (Daniel, 2026-08-12). This is a retreat to
           nothing, not a state change you have to keep up with; at the house
           0.2s it read as a blink, and even doubled it still snapped. The
           label's mask ramp below runs on the same clock, so the track and
           the ink it makes room for leave together. */
        transition: opacity var(--kik-pac-rest-fade, 1s) ease;
    }
    #kik-pac-dots.kik-pac-dots--docked.kik-pac-dots--rest {
        opacity: 0;
        pointer-events: none;
    }

    /* …and the same when it LEAVES. buildNav() sets [hidden] the moment the
       last app closes, and `display: none` is not something you can ease out
       of — the track simply blinked away mid-fade (Daniel, 2026-08-12: let it
       fade gently). `transition-behavior: allow-discrete` holds the display
       flip until the opacity has finished, and @starting-style gives it the
       same treatment on the way back in. Engines without either get exactly
       what they get today: an instant swap. */
    #kik-pac-dots.kik-pac-dots--docked {
        transition: opacity var(--kik-pac-rest-fade, 1s) ease,
                    display var(--kik-pac-rest-fade, 1s) allow-discrete;
        opacity: 1;
    }
    #kik-pac-dots.kik-pac-dots--docked[hidden] {
        /* Still display: none — allow-discrete is what holds that flip back
           until the opacity has run, so the element leaves the layout (and
           the accessibility tree) once it is actually gone. */
        display: none;
        opacity: 0;
        pointer-events: none;
    }
    @starting-style {
        #kik-pac-dots.kik-pac-dots--docked:not([hidden]) {
            opacity: 0;
        }
    }

    /* Candy box: a filled accent chip with WHITE line art while the CAROUSEL
       is up — the switcher's cards, or the lone home card stepped back —
       and transparent on the home screen and inside an app (Daniel,
       2026-09-12; it used to light on the home, when the button meant
       "apps grid"). The button opens the switcher now, and the chip says
       the switcher is what you are looking at. The front class is what
       tells the live carousel's home apart from its cards.

       Keyed on SHELL STATE (:has), not on .kik-candy-apps-active: the bento
       dashboard's own candy painter still runs in the hidden pane and strips
       that class a beat after our painter sets it — a state selector is not
       something JS can take away. The state is now "the desk is the active
       app": home used to be the ABSENCE of a foreground app
       (#app-space:not(.state-on)), which on One/Pro can no longer happen —
       the stage is always state-on and the home lives inside the dashboard
       app (2026-08-16), so that selector had gone quietly dead and the chip
       kept the bar's black glyph. The glyph colours need !important because the bar paints
       all of its iconography black at that strength (the .gns-icon.app-icon
       / .rail-icon rules in global-nav-side.css); the :has carries an id,
       so the specificity clears the bar's rules too. */
    body.kik-pac.kik-pac-switch:not(:has(#application-body-dashboard.kik-pac-front)) #global-nav-side .gns-candy-trigger .gns-icon {
        background: var(--kik-apps-accent, #F37021);
    }
    body.kik-pac.kik-pac-switch:not(:has(#application-body-dashboard.kik-pac-front)) #global-nav-side .gns-candy-trigger .gns-icon.app-icon * {
        fill: #fff !important;
    }
    body.kik-pac.kik-pac-switch:not(:has(#application-body-dashboard.kik-pac-front)) #global-nav-side .gns-candy-trigger .gns-icon.rail-icon * {
        /* White line, and the icons' own wash of white inside it — the
           chip is a phone tile now, and reads like one. */
        fill: color-mix(in srgb, #fff var(--app-icon-fill-opacity, 0%), transparent) !important;
        stroke: #fff !important;
    }
    /* (The switcher's old third state — no chip, the glyph solid accent —
       went on 2026-09-12: the mode's class is on at the home now too, so it
       painted an orange blob on the home screen, and the carousel's own
       state is the chip above.) */
    /* THE APP MENU off the dock (Daniel, 2026-08-12): tapping the boxed icon
       of the app on screen opens the app menu the header tab serves
       elsewhere — AppLabelMenuView's Preferences / Help / Favourite / Close.
       The trigger is the dock row's own menu flavour (phone_dock_apps.html);
       what is here is the PANEL, which carries its own class, deliberately
       NOT app-label-menu: that class has a phone rule in pat-context-menu.css
       pinning the panel to a fixed frame under the global header — right for
       the HEADER tab's menu, and the reason this one first drew at the top
       of the screen. */
    /* The panel itself: it rises off the dock at the bottom of the screen,
       so it is only ever as tall as the room above the bar — the pattern's
       own cap (100vh - 100px) is measured from the top of the screen and
       would let a long menu run past the icon it belongs to, at which point
       the placement clamps it to the viewport edge and the menu reads as
       detached chrome rather than as this icon's menu. The list scrolls
       inside whatever is left. `min-width: 0` is what .app-label-menu gives
       the header's copy: an app menu sizes to its own entries. */
    .context-menu-popover.kik-pac-app-menu {
        max-width: min(80vw, 320px);
    }
    .context-menu-popover.kik-pac-app-menu .context-menu-content {
        min-width: 0;
        max-height: calc(100dvh
            - var(--workspace-tabs-bar-height, var(--gns-mobile-height, 64px))
            - 40px);
    }
    /* Scrub hit area: much larger than the visible pill (the ::before is
       part of the nav's hit box but paints nothing). */
    #kik-pac-dots::before {
        content: '';
        position: absolute;
        inset: -20px -32px;
        border-radius: 999px;
    }
    #kik-pac-dots .kik-pac-dot {
        width: 9px;
        height: 9px;
        border-radius: 50%;
        border: 0;
        padding: 0;
        background: rgba(0, 0, 0, 0.28);
        cursor: default;
    }
    /* THE TRACK IS GREY: the dots say how many apps are open and where they
       sit; WHICH one you are on is the slider ball's job, in that app's own
       colour (pageColours below). The grid's white-and-orange dot left with
       the grid page, 2026-08-15. */
    /* The moving active ball: slides along the rail with the carousel's
       actual scroll progress (no hopping) and colour-blends between the
       neighbouring pages' app colours as it travels. */
    #kik-pac-dots .kik-pac-ind {
        position: absolute;
        top: 50%;
        left: 0;
        width: 16px;
        height: 16px;
        border-radius: 50%;
        background: #F37021;
        pointer-events: none;
        transform: translate(0, -50%);
    }

    /* ------------------------------------------------------ the switcher */
    /* Long-press on the candy box (Daniel, 2026-08-15). The open apps step
       back to cards — scale(0.8), a page-width apart — and the carousel
       gesture comes alive: swipe sideways to browse, swipe a card UP to
       close its app, tap a card to enter it. A short press never sees any
       of this: it teleports to the apps grid, instantly. */
    /* The transition lives on the BASE rule so leaving the switcher eases
       back to full size too — the transform itself only exists under
       .kik-pac-switch, so at rest the pages carry no transform at all (a
       scaled or even scale(1) page is a containing block for its app's own
       position:fixed chrome, which must not happen outside the mode). */
    body.kik-pac #app-space > .application-body.state-on {
        transition: transform 0.3s ease, opacity 0.18s ease,
                    border-radius 0.15s ease;
    }
    /* THE MODE'S VERTICAL RHYTHM (Daniel, 2026-08-15): one gap, three
       times. viewport top —gap— label —gap— card —gap— bar. The slack the
       0.8 scale leaves (viewport minus bar minus the 80dvh card minus the
       label's band) splits into those three gaps; max() keeps them sane on
       a screen with no room. --kik-switch-ty moves the scale-centred card
       (natural top 10dvh) onto its slot. */
    body.kik-pac-switch {
        --kik-sw-titleh: 36px;
        --kik-sw-gap: max(6px, calc((100dvh
            - var(--workspace-tabs-bar-height, var(--gns-mobile-height, 64px))
            - env(safe-area-inset-bottom, 0px)
            - 80dvh
            - var(--kik-sw-titleh)) / 3));
        --kik-switch-ty: calc(2 * var(--kik-sw-gap) + var(--kik-sw-titleh) - 10dvh);
    }
    /* The switcher shows every open app, whatever #app-space's own state —
       both .kik-pac classes for the same four-class weight as the resting
       hide above (three lost to it: entering the switcher FROM HOME left
       every card display:none — a dark stage with nothing on it), later in
       the sheet, so the mode wins. (The cards otherwise scale in place —
       the app's name and icon take the gap above each card.) */
    body.kik-pac.kik-pac-switch #app-space > .application-body.state-on {
        display: block !important;
        /* In FRONT of the label layer (z 1) on the way past — the card
           covers its own title as it flies out, not the other way round.
           The JS writes each page's own z-index (2 + its distance from the
           row's end), front first: the home covers the strip its overlap
           lays under it, and each card the next (2026-09-12). */
        z-index: 2;
        /* --kik-drag-y is the swipe-up's live finger offset — as of
           2026-09-12 only a fallback: the mode's transform is the row's
           track or the painter's inline value, both of which outrank this
           rule, so the JS writes the drag INLINE (setCardDrag) instead, and
           the close flight and the spring-back are transitions of that.
           --kik-sw-scale is measured (0.8 where there is room; smaller on
           short screens so the gaps keep their minimum). */
        transform: translateY(calc(var(--kik-switch-ty)
                                   + var(--kik-drag-y, 0px)))
                   scale(var(--kik-sw-scale, 0.8));
        /* The card wears the house corner (Daniel, 2026-08-15) against the
           dark backdrop; overflow:hidden (base rule) clips the app to it. */
        border-radius: var(--border-radii, 10px);
        /* Sideways pans stay the scroller's; vertical ones reach the JS
           (the swipe-up close) instead of scrolling the app under the
           card. */
        touch-action: pan-x;
        /* THE NEIGHBOURS PEEK IN (Daniel, 2026-08-15): a sliver of the next
           and previous card shows at the edges, so the mode reads as a row
           you can swipe, not a lone card. The scaled card paints 80% of its
           100%-wide slot (10% air each side); pulling the slots 8% over
           each other leaves 2×8−10 = 6% of each neighbour on screen.
           Snapping is untouched — the snap area is the border box, still
           100% wide, still centre-aligned. */
        margin-left: calc(-1 * var(--kik-sw-overlap, 8%));
        margin-right: calc(-1 * var(--kik-sw-overlap, 8%));
    }
    /* …but not on the row's OUTER edges: a trailing negative margin eats
       the scroll extent, and the last card came up exactly that short of
       centre (35px on a 390px phone). Only the margins BETWEEN cards make
       the peek. */
    body.kik-pac-switch #app-space
        > .application-body.state-on:not(#app-space > .application-body.state-on ~ *) {
        margin-left: 0;
    }
    body.kik-pac-switch #app-space
        > .application-body.state-on:not(:has(~ .application-body.state-on)) {
        margin-right: 0;
    }
    /* A card is a picture of the app, not the app: NOTHING inside it may
       take the tap, the hover, or the click — the tap belongs to the card
       (select) and the swipe to the shell. The whole subtree, !important:
       plain none on the children let any descendant that declares its own
       pointer-events: auto (the Films app's video did — it played inside
       the peeking card instead of bringing it forward, Daniel, 2026-08-15)
       punch itself back in. */
    body.kik-pac-switch #app-space > .application-body.state-on:not(.kik-pac-front) * {
        pointer-events: none !important;
    }
    /* THE HOME AT THE FRONT IS A PAGE (2026-09-12): the carousel is live at
       slide one too, where the home stands full size — its tiles take
       taps, its lists scroll, and the row still takes a sideways pan. The
       JS keeps the class on it while the row rests there. */
    body.kik-pac-switch #app-space > .application-body.state-on.kik-pac-front {
        touch-action: auto;
        /* Square at the front, the house corner as soon as it moves: a
           corner interpolated per frame repainted the page every frame
           (Daniel, 2026-09-12: the frame rate had dropped), so the corner
           is a state, eased once on the way out. */
        border-radius: 0;
    }
    /* No blanket will-change on the pages (2026-09-12): a dozen open apps
       made a dozen full-screen layers and the phone dropped frames. The
       pages that animate are promoted by their running track, and only the
       ones near the viewport carry it (attachCardTracks). */
    /* THE APP'S NAME AND ICON, above the card (Daniel, 2026-08-15). One
       title per open app, all STACKED on the same fixed spot — the strip
       the scaled cards free at the top of the screen — and cross-faded
       into each other as the carousel scrolls (the JS painter, or the
       scroll-driven animation where the engine has it, drives opacity from
       the same scroll fraction as the track's ball). The titles do NOT
       ride the scroller: the label holds still while the cards travel. */
    #kik-pac-titles { display: none; }
    body.kik-pac-switch #kik-pac-titles {
        display: block;
        /* FIXED — the titles hold still while the cards scroll — but
           parked INSIDE #app-space (the JS seats it there), so it paints
           in the stage's own stacking context: over the dark background
           (z 1), under the cards (z 2) — the card passes IN FRONT of its
           label on the way out. */
        position: fixed;
        top: var(--kik-sw-gap);
        left: 0;
        right: 0;
        height: var(--kik-sw-titleh);
        pointer-events: none;
        z-index: 1;
    }
    /* The home's own title is not wanted (Daniel, 2026-09-12): the wordmark
       says where you are. The element stays, hidden — the titles are
       indexed against the pages, and the tracks are built per index. */
    body.kik-pac-switch #kik-pac-titles #kik-pac-title-dashboard { display: none; }
    body.kik-pac-switch #kik-pac-titles .kik-pac-card-title {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 9px;
        /* Ink, not white: the stage is a darkened BEIGE, not a night sky
           (Daniel, 2026-08-15) — white lettering on it was barely there. */
        color: var(--colour-text, #1a1a1a);
        font-size: 17px;
        opacity: 0;
    }
    /* The app's tile, rebuilt small: colour plate, white line art — the
       same recipe the dock's boxed icon uses (phone-dock.css). */
    body.kik-pac-switch #kik-pac-titles .kpt-icon {
        display: flex;
        flex: 0 0 auto;
        width: 30px;
        height: 30px;
        border-radius: 9px;
        padding: 3px;
        box-sizing: border-box;
        overflow: hidden;
        background: var(--kpt-colour, var(--colour-accent, #F37021));
        color: #fff;
    }
    body.kik-pac-switch #kik-pac-titles .kpt-icon svg,
    body.kik-pac-switch #kik-pac-titles .kpt-icon img {
        width: 100%;
        height: 100%;
        display: block;
    }
    body.kik-pac-switch #kik-pac-titles .kpt-icon svg {
        opacity: var(--kik-app-glyph-opacity, 0.65);
    }
    body.kik-pac-switch #kik-pac-titles .kpt-icon svg,
    body.kik-pac-switch #kik-pac-titles .kpt-icon svg * {
        fill: transparent;
    }
    body.kik-pac-switch #kik-pac-titles .kpt-icon .counter { display: none; }
    body.kik-pac-switch #kik-pac-titles .kpt-name {
        white-space: nowrap;
        max-width: 70vw;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    /* A chat composer inside a CARD sits at the card's own bottom (Daniel,
       2026-08-15/16): its resting inset is the dock clearance — the trade it
       makes with the bar when the app is full-screen — which inside the
       scaled card left it floating mid-picture. In the mode there is no
       dock to clear: the card's edge is the floor.

       Retuning the inset alone (the first fix) was not enough, because in
       the card the composer often isn't STUCK at all: it is position:
       sticky in the chat's scroll box, and sticky only ever lifts an
       element UP to its sticky line from a lower flow position — it never
       pushes one DOWN. A card showing a short conversation, or one whose
       scroller sits anywhere but its own bottom, leaves the composer at its
       flow position, mid-picture, with `bottom` doing nothing at all.

       So in the mode it stops being sticky and becomes FIXED — the same
       mechanism the Ask-Kiki dock below relies on: the card's transform
       makes it the containing block, so `fixed` resolves against the CARD.
       The card's bottom edge, ten pixels up, whatever the app inside is
       doing. The side inset and the margin are restated because the phone
       rule (messages/styling) states the island's width as a margin, which
       an inset-positioned box has to express as left/right instead.

       Not the Ask-Kiki DOCK's composer (.kiki-composer): that one rides
       inside .kiki-dock, which is pinned to the card's bottom edge by the
       rule below — pinning it a second time, itself, would tear it out of
       the dock it belongs to. */
    body.kik-pac.kik-pac-switch #app-space > .application-body.state-on:not(.kik-pac-front)
        .kik-chat-composer:not(.kiki-composer) {
        --kik-composer-inset: 10px;
        position: fixed;
        left: 15px;
        right: 15px;
        bottom: 10px;
        margin: 0;
    }
    /* The Ask-Kiki dock too — and by a different mechanism: it is
       position: fixed with the dock clearance as its bottom, and inside a
       transformed card `fixed` resolves against the CARD (the transform is
       its containing block), so the viewport-tuned offset floated it at
       ~70% of the picture. Card-relative is exactly what the mode wants;
       only the numbers change: hug the card's bottom edge. */
    body.kik-pac.kik-pac-switch #app-space > .application-body.state-on:not(.kik-pac-front) .kiki-dock {
        bottom: 10px;
    }

    /* THE MAIN BAR IS THE CARD'S, NOT THE SCREEN'S (Daniel, 2026-08-16: the
       toolbars jiggle — still, after the page was given `contain: layout`).
       The house main toolbar is `position: fixed` (windowed-mode.css), and
       fixed inside this shell has TWO possible containing blocks: the card,
       while the switcher's transform is on it, and the viewport otherwise.
       The mode crosses between them constantly — entering, leaving, and for
       0.25s of eased transform each way — and a bar that resolves against
       the viewport paints 390px wide where its 0.8-scaled card paints 312.
       That difference is the jiggle: measured off a screen recording, the
       whole row jumps 73px sideways in ONE frame with the card standing
       still, then corrects twice more over the next five. Layout
       containment was meant to remove the choice, but the page is only
       sometimes the containing block on WebKit, so the flip survived.

       `absolute` has no such choice to make. The page is position: relative
       (above), so the bar resolves against the CARD on every engine, with
       or without a transform, and it paints inside the card's own layer —
       it cannot lag a frame behind the thing it sits on. It stays out of
       flow, so the content clearance windowed-mode.css hands the document
       body is unchanged, and the page's overflow: hidden clips it to the
       card's corner like the rest of the app.

       ONLY WHILE THE SWITCHER IS UP, though (Daniel, 2026-08-17). Everything
       above is about a card under a transform being swiped between: that is
       the switcher's state and nowhere else. At rest there is one app, no
       transform, no card to lag behind — and `absolute` costs the bar the one
       thing it is for, since a bar in the card's coordinate space travels with
       the content and scrolls off the top, leaving nothing to auto-hide. So
       the jiggle fix is scoped to the state that has the jiggle, and a resting
       app keeps the house `fixed` bar and its auto-hide.

       One id + eight classes, to clear the seven-class windowed rule. */
    body.kik-pac.kik-pac-switch #app-space > .application-body.state-on:not(.kik-pac-front)
        .pat-toolbar.kik-win-main-toolbar:not(.well-title-group *) {
        position: absolute;
    }

    /* Nothing to switch between: the long press still answers — the dark
       stage, and this one line on it (buildNav toggles [hidden]). */
    #kik-pac-empty { display: none; }
    body.kik-pac-switch #kik-pac-empty:not([hidden]) {
        display: block;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        /* Ink on the beige stage, softened — see the card titles above. */
        color: color-mix(in srgb, var(--colour-text, #1a1a1a) 70%, transparent);
        font-size: 17px;
        text-align: center;
        pointer-events: none;
        max-width: 80vw;
    }
    /* While the finger drives, nothing eases — frame-locked like the media
       player's dismiss swipe. */
    body.kik-pac-switch #app-space > .application-body.state-on.kik-pac-dragging {
        transition: none;
    }
    /* Far or fast enough: the FLIGHT — the card fades and keeps sliding out
       at the same time, departing from exactly where the finger let go (the
       JS pushes --kik-drag-y off the top of the screen; the transform
       transition carries it). */
    body.kik-pac-switch #app-space > .application-body.state-on.kik-pac-closing {
        opacity: 0;
        transition: transform 0.22s ease-in, opacity 0.22s ease-in;
    }
    /* …after which the SLOT it occupied collapses to nothing with a
       transition, the neighbours sliding over — only then does the real
       close (class flips, server persist) remove the element, invisibly. */
    body.kik-pac-switch #app-space > .application-body.state-on.kik-pac-collapsing {
        flex-basis: 0;
        min-width: 0;
        margin-left: 0;
        margin-right: 0;
        transition: flex-basis 0.25s ease, min-width 0.25s ease,
                    margin 0.25s ease,
                    transform 0.22s ease-in, opacity 0.22s ease-in;
    }
    /* …but a collapsing card at either END of the row keeps a POSITIVE
       margin there (Daniel, 2026-08-15: the next app slid in a bit too far
       and snapped back). Its neighbour still carries its own −overlap while
       the collapse plays, yet the moment the element leaves the DOM the
       neighbour becomes the row's edge and the edge rule zeroes that margin
       — an 8% jump at the very end of an otherwise smooth slide. Cancelling
       the neighbour's overlap for the duration lands it exactly where
       removal will put it, so the whole movement is one continuous glide.
       (A card in the MIDDLE needs none of this: zeroed margins already put
       its successor precisely where the removal does.) */
    body.kik-pac-switch #app-space
        > .application-body.state-on.kik-pac-collapsing:not(#app-space > .application-body.state-on ~ *) {
        margin-right: var(--kik-sw-overlap, 8%);
    }
    body.kik-pac-switch #app-space
        > .application-body.state-on.kik-pac-collapsing:not(:has(~ .application-body.state-on)) {
        margin-left: var(--kik-sw-overlap, 8%);
    }

    /* The bar hands its strip to the slider: the media chip and the dock's
       app row fade out for the duration of the mode — and the whole
       hand-over runs on the track's own 1s clock (Daniel, 2026-08-15): the
       track fades in as the row and the chip fade out, and the reverse on
       the way back. The JS painter drives the same two per frame while the
       row is in flight (liveChrome). */
    body.kik-pac .kik-media-player-chip,
    body.kik-pac #global-nav-side > #kik-dock-apps {
        transition: opacity var(--kik-pac-rest-fade, 1s) ease;
    }
    /* The track's own fade-in: it flips from display:none as the mode
       arrives, so @starting-style supplies the opacity-0 first frame; the
       docked rule's existing `display … allow-discrete` transition covers
       the fade OUT on the way back (engines without either simply swap). */
    body.kik-pac-switch #kik-pac-dots { opacity: 1; }
    @starting-style {
        body.kik-pac-switch #kik-pac-dots {
            opacity: 0;
        }
    }
    /* Not !important any more (2026-09-12): the fade rides the row's
       scroll timeline (or the JS painter's inline value), and an animation
       cannot drive an !important declaration. This is the value one slot
       in and beyond; at the front the row is the home's, and alive. */
    body.kik-pac-switch .kik-media-player-chip,
    body.kik-pac-switch #global-nav-side > #kik-dock-apps {
        opacity: 0;
    }
    body.kik-pac-switch:not(:has(.kik-pac-front)) .kik-media-player-chip,
    body.kik-pac-switch:not(:has(.kik-pac-front)) #global-nav-side > #kik-dock-apps {
        pointer-events: none !important;
    }

    /* The slider sits in the MIDDLE of the bar (Daniel, 2026-08-15), and it
       may never grow wide enough to reach the candy box. The candy box's
       left edge is 58px in from the right rim (8px bar padding + its 50px
       hit area — the same 58px the media chip's perch is measured from),
       plus the 15px of air Daniel asked for; the box is centred, so the cap
       mirrors that clearance on both sides of the midpoint. The base docked
       rule already seats it centred — this only caps the width. */
    body.kik-pac-switch #kik-pac-dots.kik-pac-dots--docked {
        max-width: calc(100% - 2 * (58px + 15px));
        flex-wrap: nowrap;
        justify-content: center;
    }
    /* While the switcher is up the track never rests — it is the mode's
       one control and fading it out from under the finger would read as
       the mode ending. Belt to the JS's braces (armRailHide bails). */
    body.kik-pac-switch #kik-pac-dots.kik-pac-dots--docked.kik-pac-dots--rest {
        opacity: 1;
        pointer-events: auto;
    }

    /* ON THE APPS GRID THE BAR STAYS UP (Daniel, 2026-08-15). The bar
       auto-hides on a downward scroll (pat-scroll-box's
       own scroll-down on <body>, read by global-nav-side.css), which is right inside an app —
       content wants the screen — but the grid IS the navigation surface and
       its bar holds the grid's own control, the candy box; scrolling a long
       grid must not take the chrome with it. Same "grid is the page in
       view" condition the candy-box highlight keys on. The switcher is
       pinned too: the bar carries the mode's slider. */
    body.kik-pac:has(.application-body.state-on.focus-document .document-body:is(.scroll-position-bottom, .scroll-down:not(.scroll-position-top))):has(#app-space:not(.state-on)) #global-nav-side,
    body.kik-pac:has(.application-body.state-on.focus-sidebar .sidebar-content:is(.scroll-position-bottom, .scroll-down:not(.scroll-position-top))):has(#app-space:not(.state-on)) #global-nav-side,
    body.kik-pac-switch:has(.application-body.state-on.focus-document .document-body:is(.scroll-position-bottom, .scroll-down:not(.scroll-position-top))) #global-nav-side,
    body.kik-pac-switch:has(.application-body.state-on.focus-sidebar .sidebar-content:is(.scroll-position-bottom, .scroll-down:not(.scroll-position-top))) #global-nav-side {
        transform: none;
    }
    /* …and the composer-focus step-aside is pinned too (Daniel, 2026-08-15:
       Kiki's autofocused composer still held :focus-within in the mode and
       the whole bar — track included — slid away). In the switcher the bar
       IS the mode's chrome; it never leaves. Same specificity as the
       global-nav-side.css rule it answers, and later in the cascade. */
    body.kik-pac-switch:has(.kik-chat-composer:focus-within) #global-nav-side {
        transform: none;
    }

    /* THE DOCK STAYS (Daniel, 2026-09-20). It used to slide out on a home
       with nothing else open (2026-09-12) on the argument that there was
       nothing to switch between — but the bar is not only the switcher any
       more: it carries the dock's own app icons, which are the launcher a
       home screen is most often reached for, and a strip of controls that
       comes and goes with what happens to be open is one the hand cannot
       learn. What the state still decides is the SCROLLER below, which has
       nothing to travel to — the rules that follow. */
    /* ONE PANE AT A TIME IS RENDERED (Daniel, 2026-09-21, measured).
     *
     * At rest the carousel shows exactly one app: the scroller cannot travel
     * sideways here (the rules below), so every other open pane is parked off
     * the screen. They stay in the DOM on purpose — that is what makes
     * switching apps instant — but they were also still being STYLED, and on
     * this shell that is not free: a phone with seventeen apps open carries
     * about seventeen thousand elements, of which the app on screen is under
     * two.
     *
     * What made that expensive rather than merely wasteful is :has(). The
     * house stylesheets carry ~1,200 :has() rules, and with any of them in
     * play the engine cannot keep an insertion's consequences local — so
     * every element an app draws (FullCalendar rebuilds thousands on each
     * month/week/day switch) is charged against the whole document. Measured
     * on a phone viewport with the CPU throttled to phone class: a calendar
     * view switch went 964ms → 770ms with the parked panes taken out of the
     * render tree, and 326ms with the :has() rules parked as well, which is
     * the remaining work and a separate piece.
     *
     * ``content-visibility: hidden`` and not ``display: none``: it skips the
     * rendering and nothing else. The pane keeps its box (it is a flex item
     * with a fixed basis, so the carousel's geometry is untouched — checked:
     * scrollWidth and every pane's rect are identical either way), its scroll
     * positions, its state and its scripts; coming back is a render, not a
     * re-mount.
     *
     * WHICH pane is on screen is ``kik-active-pane``
     * (libraries/kikaron/active-pane), the mirror of the shell's own
     * ``active-app-<slug>``, and it fails safe: with that script absent
     * nothing is marked, so nothing is hidden. The switcher is excluded
     * because that is exactly when the neighbours are meant to be seen, and
     * ``#app-space.state-on`` keeps it off the launcher page.
     */
    body.kik-pac:not(.kik-pac-switch) #app-space.state-on
        > .application-body.state-on:not(.kik-active-pane):not(.kik-pac-front) {
        content-visibility: hidden;
    }

    /* A home with nothing else open is not a row (Daniel, 2026-09-12: a
       horizontal scroll bar under the grid): nothing to swipe to, so the
       scroller takes no sideways gesture and shows no bar. The timeline is
       only ever born once an app is open, so the hidden overflow here
       cannot wedge it. */
    body.kik-pac.kik-pac-desk-alone #app-space {
        overflow-x: hidden;
        scroll-snap-type: none;
    }
    body.kik-pac.kik-pac-desk-alone #app-space > .application-body.state-on,
    body.kik-pac.kik-pac-desk-alone #app-space > .application-body.state-on > .document-body {
        overflow-x: hidden;
    }

    /* The candy box is now a press-and-hold control: no iOS link preview,
       no text-selection callout on the hold. */
    body.kik-pac #compact-candy-box,
    body.kik-pac #global-nav-side .gns-candy-trigger {
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        user-select: none;
    }
    /* A THUMB-SIZED HIT AREA (Daniel, 2026-08-15): the candy box owns its
       whole corner of the bar — up to the bar's top edge, out to the right
       and bottom of the viewport (safe area included), and a good 40px
       further left. The ::before paints nothing; it is hit box only, and
       clicks/long-presses on it resolve to the trigger via closest(). */
    body.kik-pac #global-nav-side .gns-candy-trigger {
        position: relative;
    }
    body.kik-pac #global-nav-side .gns-candy-trigger::before {
        content: '';
        position: absolute;
        top: -8px;                                     /* the bar's top edge */
        right: -8px;                                   /* the bar's own padding */
        bottom: calc(-8px - env(safe-area-inset-bottom, 0px));
        left: -40px;
    }
    /* …but the media chip, which perches immediately left of the candy box,
       stays on top of that extension: its own taps are its own. */
    body.kik-pac .kik-media-player-chip {
        z-index: 2;
    }

    /* --- The one-time hint -----------------------------------------------
       A context-menu popover in everything but name: the same tooltip tokens
       for the surface, the same frost, the same beak drawn as a rotated
       square carrying the panel's own colour. It is positioned by the JS
       (fixed left/top against the live candy box) for the same reason the
       context menu positions itself in script — the button moves with the
       layout, and CSS anchor positioning is not yet everywhere.

       Above the bar's own z-index so it is not clipped by it, and
       pointer-events only on the × : the bubble must not swallow the very
       long press it is describing. */
    body.kik-pac .kik-pac-hint {
        position: fixed;
        z-index: 60;
        /* A stated width, not a max: the bubble hangs inside the desk's own
           body, whose boxes (the apps grid, a card) would otherwise decide how
           wide "shrink to fit" is — it came out a 156px column of wrapped
           words. One line of advice on a phone is as wide as the phone. */
        width: min(var(--pat-context-menu-max-width, 350px), calc(100vw - 24px));
        box-sizing: border-box;
        display: flex;
        align-items: flex-start;
        gap: 0.5rem;
        padding: var(--pat-context-menu-padding, 15px);
        border-radius: var(--pat-context-menu-radii);
        border: var(--pat-context-menu-border, none);
        box-shadow: var(--pat-context-menu-shadow, var(--default-box-shadow));
        background-color: var(--pat-context-menu-tip-colour, #fff);
        -webkit-backdrop-filter: var(--pat-context-menu-backdrop-filter, var(--pat-toolbar-button-backdrop-filter));
        backdrop-filter: var(--pat-context-menu-backdrop-filter, var(--pat-toolbar-button-backdrop-filter));
        color: var(--body-font-colour);
        font-size: var(--body-font-size);
        pointer-events: none;
        animation: kik-pac-hint-in 0.24s ease both;
    }
    @keyframes kik-pac-hint-in {
        from { opacity: 0; transform: translateY(6px); }
        to   { opacity: 1; transform: none; }
    }
    /* Out of the way while the home is being scrolled (Daniel, 2026-08-16).
       The bubble is fixed and its beak points at a button on the dock, so it
       is the one thing on a scrolling screen that does not move — it sat over
       the apps travelling under it and read as stuck to the glass. Scrolling
       down slides it a little further down and fades it out; coming back to
       the top brings it back.

       Faded, NOT dismissed: scrolling past a hint is not learning what it
       says, so nothing is recorded and the bubble is still owed to the reader.
       Only the × and the gesture itself close it for good.

       The transition lives on the base rule so the return animates too, and
       the JS clears the intro animation before first use — an animation with
       `fill: both` keeps its final transform after it ends, which would
       outrank anything a class tried to set here. */
    body.kik-pac .kik-pac-hint {
        transition: opacity 0.2s ease, transform 0.2s ease;
    }
    body.kik-pac .kik-pac-hint.kik-pac-hint-away {
        opacity: 0;
        transform: translateY(16px);
    }
    @media (prefers-reduced-motion: reduce) {
        body.kik-pac .kik-pac-hint { animation: none; transition: none; }
        body.kik-pac .kik-pac-hint.kik-pac-hint-away { transform: none; }
    }
    /* The beak: a rotated square in the panel's own colour, sitting on the
       bottom edge under the bubble, its offset set by the JS so it points at
       the candy box wherever that ended up. */
    body.kik-pac .kik-pac-hint::after {
        content: '';
        position: absolute;
        bottom: calc(-0.5 * var(--pat-context-menu-arrow-size, 11px));
        left: var(--kik-pac-hint-arrow, 50%);
        width: var(--pat-context-menu-arrow-size, 11px);
        height: var(--pat-context-menu-arrow-size, 11px);
        margin-left: calc(-0.5 * var(--pat-context-menu-arrow-size, 11px));
        background-color: var(--pat-context-menu-tip-colour, #fff);
        transform: rotate(45deg);
    }
    body.kik-pac .kik-pac-hint-text {
        margin: 0;
        line-height: 1.3;
    }
    body.kik-pac .kik-pac-hint-close {
        pointer-events: auto;
        flex: none;
        /* The one thing in the bubble that IS a target, so it gets a real
           one: 44px of it, with the glyph drawn at its own size inside. */
        width: 44px;
        height: 44px;
        margin: -0.5rem -0.5rem -0.5rem 0;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        border: 0;
        background: none;
        color: inherit;
        opacity: 0.7;
    }
    /* HOME only. The bubble lives in the desk's app body so it belongs to the
       desk — but in this mode every open app is a page laid beside it, and a
       fixed child is not clipped by the page it sits in, so it would hang over
       Mail all the same. Two ways to be looking at the home since the desk
       became an app: nothing active at all, or the desk itself active.

       And never over the switcher, which replaces the screen — the long press
       dismisses the hint anyway, but the class flips before the POST lands. */
    body.kik-pac .kik-pac-hint {
        display: none;
    }
    body.kik-pac:has(#app-space:not(.state-on)) .kik-pac-hint,
    body.kik-pac:has(#app-space.active-app-dashboard) .kik-pac-hint {
        display: flex;
    }
    /* …but not over the cards: the carousel is live at home too now
       (2026-09-12), so "in the switcher" is the row away from the front. */
    body.kik-pac-switch:not(:has(.kik-pac-front)) .kik-pac-hint,
    body.kik-pac-switch:not(:has(.kik-pac-front)):has(#app-space:not(.state-on)) .kik-pac-hint,
    body.kik-pac-switch:not(:has(.kik-pac-front)):has(#app-space.active-app-dashboard) .kik-pac-hint {
        display: none;
    }
}
