/* Side global navigation (#global-nav-side).
 *
 * Everything global-nav related lives in THIS file. Every selector is
 * namespaced: #global-nav-side, .gns-*, body[data-global-nav],
 * body.gns-collapsed.
 *
 * The rail is built to work exactly like the workspaces app's
 * .workspace-tabs column: a TRANSPARENT floating icon column,
 * SUPERIMPOSED on top of the app canvas (no panel, no border, no
 * content displacement). #app-space / #portal stay at left: 0; instead
 * the app content clears the rail the same way workspace content
 * clears its tabs column:
 *   - sidebars get the workspace-style left padding
 *     (--sidebar-padding with the 50px rail inset);
 *   - document bodies get --document-body-left-distance, consumed by
 *     the stock .document-body rules in all.css.
 *
 * Mode is a body state: <body data-global-nav="top|side">, rendered
 * server-side so there is no flash of the wrong nav. The rail is
 * expanded (--workspace-tabs-home-width) on Home only: whenever an app
 * is on screen (#app-space.state-on, matched via :has, plus the
 * body.gns-collapsed class KikaronGlobalNav mirrors) it collapses to
 * the 50px icon column — desktop only; under 768px the bottom tab bar
 * takes over.
 */

/* Registered as a real <color> so app-to-app switches interpolate —
 * the collapsed rail's wash then fades between app colours instead of
 * snapping (the var is stamped on <body> by KikaronGlobalNav). */
@property --gns-active-app-colour {
    syntax: '<color>';
    inherits: true;
    initial-value: transparent;
}

body[data-global-nav="side"] {
    transition: --gns-active-app-colour var(--gns-transition);
}

:root {
    /* Favourites runs as a rail app but has no App registry row, so its
     * colour isn't in the generated apps.css — give it the accent. */
    --app-colour-favourites: var(--colour-accent, #ea6c1a);
    /* 20px narrower than the workspace-tabs column: the expanded rail's
     * labels don't need the full 250px, and the dashboard feed (which
     * clears the rail via this var) gets the reclaimed width. */
    --gns-width: calc(var(--workspace-tabs-home-width, 250px) - 20px);
    --gns-width-collapsed: 50px;
    --gns-mobile-height: 64px;
    /* Corner radius of the active-app tile and the Home brandmark tile on
     * the mobile rail — shared so the two stay visually identical. */
    --gns-mobile-active-tile-radius: 13px;
    /* same x/y inset the workspace-tabs column uses */
    --gns-left: calc(var(--sidebar-margin-left, 15px) + 0.5 * var(--sidebar-padding-right, 25px));
    --gns-top: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
    --gns-background: #fff;
    --gns-hairline: var(--colour-separation-lines, #eae2d6);
    --gns-ink: var(--colour-text, #1a1a1a);
    --gns-muted: var(--colour-discreet, #6b6560);
    --gns-accent: var(--colour-accent, #ea6c1a);
    /* One speed/curve for every rail transition (width morph, logo, labels,
     * fades, colour washes) so expand/collapse and all their parts move
     * together. */
    --gns-transition: var(--base-animation-duration, 0.3s);
}

/* ------------------------------------------------------------------ */
/* The rail itself — workspace-tabs look: transparent floating column  */
/* ------------------------------------------------------------------ */

#global-nav-side {
    display: none;
    position: fixed;
    top: var(--gns-top);
    bottom: calc(var(--sidebar-margin-bottom, 15px) + 0.5 * var(--sidebar-padding-bottom, 25px));
    left: var(--gns-left);
    width: var(--gns-width);
    /* transparent while expanded; the collapsed state paints it white */
    border: 1px transparent solid;
    border-radius: calc(0.7 * var(--border-radii, 8px));
    padding-top: 22px;
    box-sizing: border-box;
    flex-direction: column;
    background: transparent;
    /* zero-size, zero-alpha shadow so the collapsed state's
     * --default-box-shadow fades in instead of popping */
    box-shadow: 0 0 0 rgba(0, 0, 0, 0);
    color: var(--gns-ink);
    z-index: 999; /* above #portal (10) / #app-space (11) and any sidebar chrome, below modals/tooltips */
    transition: all var(--gns-transition);
    will-change: width;
    overflow-x: hidden;
    /* No scrollbar in the vertical rail: anything that doesn't fit moves into
     * the "More apps" candy box instead (global-nav-side.js
     * syncAppsOverflow / syncSpacesOverflow measure and mark). A scrolling
     * rail hides its own contents behind a gesture — the overflow menu is the
     * affordance, so clip rather than scroll. */
    overflow-y: hidden;
}

body[data-global-nav="side"] #global-nav-side {
    display: flex;
}

/* One and Pro get the SAME bottom bar on phones (Daniel, 2026-08-11).
 *
 * Compact mode's own rail (cloud_nav_rail.html) is desktop-only, so below
 * 769px those faces had no rail at all — the app carousel used to stand in
 * for it, and that is gone. The markup needs nothing: global_nav_side.html is
 * already included on every authenticated full page regardless of mode (it is
 * this `display` rule, and nothing else, that keeps it off the other faces).
 * So show it here, and the ≤768px bottom-bar section at the foot of this file
 * matches [data-global-nav="compact"] alongside "side" for the styling.
 *
 * Above the breakpoint compact keeps its own rail and this stays hidden. */
@media only screen and (max-width: 768px) {
    body[data-global-nav="compact"] #global-nav-side {
        display: flex;
    }
}

@media only screen and (min-width: calc(768px + 1px)) {
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side {
        width: var(--gns-width-collapsed);
        /* Fixed semi-transparent white, the same regardless of which app
         * is active (previously a 10% wash of the active app/space colour). */
        background: rgba(255, 255, 255, 0.5);
        box-shadow: var(--default-box-shadow);
        border-color: white;
    }
}

/* Header: platform identity + the user (personal-menu trigger).
 * interpolate-size lets the auto height animate to the collapsed 31px. */
#global-nav-side .gns-header {
    flex: 0 0 auto;
    height: 120px;
    padding: 0px 0 6px 10px;
    interpolate-size: allow-keywords;
    transition: all var(--gns-transition);
}
/* Platform logo: full wordmark expanded, brandmark (swirl) collapsed —
 * the two images sit stacked and crossfade with the width animation */
#global-nav-side .gns-platform-logo {
    display: block;
    position: relative;
    height: 67px;
    cursor: default;
    /* The logo tile box morphs between the full and collapsed states —
     * transition everything on the same rail speed so it eases instead of
     * snapping while the wordmark img morphs. */
    transition: all var(--gns-transition);
}
#global-nav-side .gns-platform-logo img {
    display: block;
    /* On the base (not just the collapsed state) so the morph uses the same
     * origin collapsing and expanding — keeps the two directions symmetric. */
    transform-origin: left;
    transition: all var(--gns-transition);
}
#global-nav-side .gns-logo-full {
    /* Rail width minus the header's 10px left padding, so the wordmark
     * tracks --gns-width instead of baking in the old 250px rail. */
    width: calc(var(--gns-width) - 10px);
}
#global-nav-side .gns-logo-mark {
    position: absolute;
    top: 0;
    left: 0;
    height: 28px;
    width: 28px;
    opacity: 0;
}
#global-nav-side .gns-platform-name {
    display: block;
    font-weight: 700;
    font-size: 1.1em;
    color: var(--gns-ink);
    text-decoration: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: all var(--gns-transition);
}
#global-nav-side .gns-hub {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin-top: 6px;
    margin-left: -8px;
    padding: 6px 8px;
    border-radius: 12px;
    box-sizing: border-box;
    max-height: 56px;
    overflow: hidden;
    min-width: 0;
    text-decoration: none;
    cursor: default;
    /* Collapse via opacity (linear fade) + max-height (linear clip) rather
     * than zoom: scaling to ~0 reads as "gone" almost instantly one way but
     * grows in gradually the other, so collapse and expand looked
     * lopsided. A fade + clip looks the same in both directions. */
    transition: all var(--gns-transition);
}

/* Hover wash + rounded corners matching the app rows (.gns-link:hover). */
@media (hover: hover) {
  #global-nav-side .gns-hub:hover {
      background: color-mix(in srgb, var(--gns-ink) 5%, transparent);
  }
}
body[dir="rtl"] #global-nav-side .gns-hub {
    margin-left: 0;
    margin-right: -8px;
}
/* The small round avatar left of the name — stock .pat-avatar shape
 * (50% radius, initials fallback), just sized down */
#global-nav-side .gns-user-avatar {
    flex: 0 0 auto;
    width: 29px;
    height: 29px;
    margin-left: -2px;
    margin-right: 1px;
    padding: 0;
    font-size: 0.68em;
    font-weight: 700;
}
/* Name stacked above the current-hub byline, to the right of the avatar. */
#global-nav-side .gns-hub-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    transition: all var(--gns-transition);
}
/* Name + down-arrow on one line; the arrow (the personal-menu dropdown
 * affordance) sits right after the name and never gets clipped. */
#global-nav-side .gns-hub-name {
    display: flex;
    align-items: center;
    min-width: 0;
    font-size: 0.85em;
    line-height: 1.2;
    color: var(--gns-ink);
    transition: all var(--gns-transition);
}
#global-nav-side .gns-hub-name-text {
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
#global-nav-side .gns-hub-name::after {
    flex: 0 0 auto;
    font-family: fontello;
    content: var(--glyph-down-open);
    font-size: 0.8em;
    color: var(--gns-muted);
    margin-left: 4px;
    transition: all var(--gns-transition);
}
/* The hub the user is currently in — a small muted byline under the name. */
#global-nav-side .gns-hub-byline {
    font-size: 0.7em;
    line-height: 1.2;
    color: var(--gns-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: all var(--gns-transition);
}
@media only screen and (min-width: calc(768px + 1px)) {
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-name {
        zoom: 0.001;
        opacity: 0;
    }
    /* The user line shrinks away rather than popping out */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-hub {
        margin-top: 0;
        opacity: 0;
    }
    /* Morph the full wordmark into the brandmark: scale down + slide left so
     * the orange swirl (on the right of the wordmark) arrives roughly where
     * the black mark sits, fading out as the mark fades in — reads as one
     * mark transforming rather than a hard crossfade. */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-logo-full {
        opacity: 0;
        transform: scale(0.30) translate(-125px, -22px);
    }
    /* RTL: the rail (and mark) sit on the right, so slide the wordmark the
     * other way. */
    body[dir="rtl"][data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-logo-full {
        transform: scale(0.9) translateX(82px);
    }
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-logo-mark {
        opacity: 1;
        top: 5px;
        left: 6px;
        /* ink-coloured brandmark, slightly inset like the app glyphs */
        filter: grayscale(1) brightness(0.08);
        transform: scale(0.92);
    }
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-header {
        padding-left: 0;
        padding-right: 0;
        height: 35px;
    }
    /* Collapsed: the logo behaves like an icon tile — same size and
     * radius as the app icons, with the same light hover wash */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-logo {
        width: 38px;
        height: 38px;
        margin-left: 6px;
        border-radius: 10px;
    }
    @media (hover: hover) {
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-logo:hover {
          background-color: color-mix(in srgb, var(--gns-ink) 7%, transparent);
      }
    }
    
    /* Home is the active screen while the rail is collapsed (only the
     * tablet-portrait case — on desktop the rail collapses only once an
     * app is on screen). The logo tile then reads as the "current"
     * destination, exactly like a current app icon: solid accent fill,
     * brandmark inverted to white. */
    body[data-global-nav="side"].gns-collapsed:not(:has(#app-space.state-on)) #global-nav-side .gns-platform-logo {
        background-color: var(--colour-accent, var(--gns-accent));
    }

    @media (hover: hover) {
      body[data-global-nav="side"].gns-collapsed:not(:has(#app-space.state-on)) #global-nav-side .gns-platform-logo:hover {
          background-color: var(--colour-accent, var(--gns-accent));
      }
    }
    body[data-global-nav="side"].gns-collapsed:not(:has(#app-space.state-on)) #global-nav-side .gns-logo-mark {
        /* invert the brandmark to solid white (vs the dark-ink filter
         * the inactive collapsed state uses above) */
        filter: brightness(0) invert(1);
    }
}

/* Item lists */
#global-nav-side ul {
    list-style: none;
    margin: 0;
    padding: 0;
}
#global-nav-side .gns-apps {
    flex: 0 0 auto;
}
#global-nav-side .gns-spaces {
    /* Directly BELOW the apps, not parked at the bottom: the spaces are the
     * apps' near neighbours — you move between an app and a space constantly —
     * and a column-length gap between them made them read as unrelated
     * furniture. A dim hairline above keeps the two groups distinct. */
    /* 0 0 auto, NOT 0 1 auto: this section is never silently squeezed. It
     * either shows its spaces at natural height or collapses wholesale to the
     * single Team spaces icon (.gns-spaces-overflow) — shrinking just clipped
     * the list against the rail's hidden overflow, hiding spaces with no
     * affordance to reach them. The collapse order lives in
     * global-nav-side.js refresh(): spaces first, then apps into the candy
     * box. */
    flex: 0 0 auto;
    margin-top: 0;
    /* No padding-top: the rule below owns the air above the first space, so
     * the open-apps group has the same 8px under it as the separator above it
     * gives it. */
    padding: 0 0 8px;
    min-height: 0;
    /* Clipped, not scrolled — see the rail's own overflow note above. Too many
     * spaces collapse to the single Team spaces icon (.gns-spaces-overflow). */
    overflow-y: hidden;
}
/* The rule between the apps and the spaces — the SAME line the apps list
 * draws between its pinned and its open rows (.gns-apps-separator): 2px at
 * 10% ink, inset 5px to the tile edge, 8px of air either side. Drawn here
 * rather than as a border on the section so the inset matches; hidden when
 * there are no spaces, since a rule under the last app with nothing below it
 * reads as a mistake (same reasoning as the separator's own :has gate). */
#global-nav-side .gns-spaces::before {
    content: "";
    display: block;
    height: 0;
    margin: 8px 5px;
    border-top: 2px solid color-mix(in srgb, var(--gns-ink) 10%, transparent);
}
#global-nav-side .gns-spaces:not(:has(.gns-item.gns-space))::before {
    display: none;
}
/* The app-launcher (candy-box) entry, between Team spaces and the personal
 * menu. It carries the margin-top:auto that .gns-spaces used to — the spaces
 * now sit straight under the apps, so this row and the personal menu are what
 * hold the bottom of the column; never squeezed, for the same reason
 * .gns-spaces isn't. (The list is .gns-app-launcher; the unqualified
 * .gns-launcher is the launcher CONTROL inside every app row — see the close
 * swap below.) */
#global-nav-side .gns-app-launcher {
    flex: 0 0 auto;
    margin-top: auto;
}
/* Mobile bottom bar: the launcher already lives there as the "More apps"
 * popover, so this desktop row stays out of the bar (same treatment the
 * personal menu gets). */
@media only screen and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-app-launcher {
        /* Both faces: the bar has its own launcher control at the right rim,
           and the rail's separate app-launcher entry is a SECOND waffle
           beside it (seen on Pro, 2026-08-12 — One's app set happens not to
           carry the entry, which is why it only showed there). */
        display: none;
    }
}
/* Personal cloud — the row between the app launcher and the personal menu.
 * No margin-top:auto of its own: the launcher above already holds the bottom
 * group down, and this list rides along under it. Never squeezed, same as its
 * two neighbours. */
#global-nav-side .gns-personal-cloud {
    flex: 0 0 auto;
}
/* Mobile bottom bar: out of the bar, like the launcher and the personal menu —
 * the More popover carries this entry there. */
@media only screen and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-personal-cloud {
        display: none;
    }
}

/* Close-on-hover for the ACTIVE app — the pro/One rail's behaviour
 * (cloud-nav-rail.css). Every app row carries a third control alongside its
 * two launcher flavours; it hides until the generated :has() rules
 * (global_nav_side.html) swap it in for the launcher while its app is the
 * on-screen one. The compound .gns-link.gns-close is deliberate: the plain
 * .gns-close form ties with .gns-link's own display and would lose on source
 * order, so the control would never hide. */
/* The candy-box entry (global_nav_side.html) is the COMPACT MOBILE BAR's
 * alone — the desktop rail has the Apps launcher row, and hub's bar has its
 * More popover. Revealed in the ≤768px block at the foot of this file. */
#global-nav-side .gns-candy-item {
    display: none;
}

#global-nav-side .gns-link.gns-close {
    display: none;
}
/* The close control carries TWO glyphs: the app icon (default) and the ×
 * (.gns-close-glyph, a fontello icon-cancel span). Hover swaps them, so the
 * active app's row reads "this closes the app" before the click. No hover
 * media query around the base hide: on touch a tap on the active row closes
 * too, and the × simply never previews. */
#global-nav-side .gns-close .gns-close-glyph {
    display: none;
    font-size: 15px;
    color: #fff;
}
#global-nav-side .gns-close .gns-close-glyph::before {
    margin: 0;
    width: auto;
}
/* The × is white, so the tile under it must be the app's colour whenever the
 * close control is on screen. The .current painting further up can't do it: it
 * keys off a SERVER snapshot (prefs_active_app), while this control is swapped
 * in by live :has() state — after a pane switch the two disagreed and the ×
 * painted white on the rail's own background, i.e. invisible. The control only
 * ever displays while its app is the active one, so painting it unconditionally
 * here is the same statement, made where it can't drift. --app-colour is
 * stamped inline per row. */
#global-nav-side .gns-close .gns-icon.app-icon {
    background-color: var(--app-colour, var(--gns-accent));
}
#global-nav-side .gns-close .gns-icon.app-icon * {
    fill: #fff !important;
}
#global-nav-side .gns-close .gns-icon.rail-icon * {
    fill: none !important;
    stroke: #fff !important;
}

@media (hover: hover) {
    #global-nav-side .gns-close:hover .gns-icon:not(.gns-close-glyph) {
        display: none;
    }
    #global-nav-side .gns-close:hover .gns-close-glyph {
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}
#global-nav-side .gns-section-title {
    display: none;
    margin: 4px 4px 6px;
    font-size: 0.7em;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--gns-muted);
    transition: all var(--gns-transition);
}
@media only screen and (min-width: calc(768px + 1px)) {
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-section-title {
        zoom: 0.001;
        opacity: 0;
    }
    /* Updates is the dashboard's home surface on desktop — only the
     * mobile bottom bar gets its own Updates tab */
    body[data-global-nav="side"] #global-nav-side .gns-item.gns-app-updates {
        display: none;
    }
    /* The utility apps (preferences / administrator / password / help) ride
     * the avatar/personal menu, not the desktop rail — they stay available on
     * the mobile bottom bar's more-apps popover.
     *
     * …unless the user PINNED one. Pinning is an explicit "keep this one
     * click away", and an admin who stars Administrator has said exactly that;
     * silently dropping it made the star a lie. Same data-gns-pinned marker
     * and same reasoning as Team spaces (global-nav-mobile-order.css) —
     * default-hidden, pinned-visible. Unpinned still never shows here, not
     * even while the app is open, since these slugs stay in
     * quaive_tags._GNS_DESKTOP_HIDDEN_SLUGS and so get no open-reveal rule. */
    body[data-global-nav="side"] #global-nav-side .gns-apps > .gns-item:is(
        .gns-app-preferences,
        .gns-app-administrator,
        .gns-app-password,
        .gns-app-help
    ):not(:has([data-gns-pinned])) {
        display: none;
    }
    /* Open-or-pinned rail (the pro/One rail's behaviour): an app that is
     * neither a favourite nor open earns no desktop rail row. The template
     * flags those entries .gns-open-item and generates a per-app :has()
     * reveal that overrides this the moment the app body is on — see
     * global_nav_side.html. Both live behind this desktop breakpoint: the
     * mobile bottom bar keeps listing every app, capped by its own
     * nth-child rules. */
    body[data-global-nav="side"] #global-nav-side .gns-apps > .gns-item.gns-open-item {
        display: none;
    }

    /* Two halves: the apps you PINNED, then the ones that are merely open.
     *
     * Done with `order`, not DOM order: the mobile bar's icon cap is
     * nth-child-based, so it counts DOM position and the list must keep the
     * order quaive_tags.global_nav_apps hands over. Flex is what makes `order`
     * work on the rows, and a column flex box lays them out exactly as the
     * block list did. Desktop only — the mobile bar dissolves this same ul
     * with display: contents. */
    body[data-global-nav="side"] #global-nav-side .gns-apps {
        display: flex;
        flex-direction: column;
    }
    body[data-global-nav="side"] #global-nav-side .gns-apps > .gns-item {
        order: 1;
    }
    body[data-global-nav="side"] #global-nav-side .gns-apps > .gns-apps-separator {
        order: 2;
    }
    body[data-global-nav="side"] #global-nav-side .gns-apps > .gns-item.gns-open-item {
        order: 3;
    }
    /* The rule itself: a dim hairline with air either side, hidden until the
     * generated :has() rules say something is actually below it (an empty
     * rule floating under the pinned apps reads as a mistake). It is a bare
     * <li> with no row of its own, so it can't be tabbed to or hovered. */
    body[data-global-nav="side"] #global-nav-side .gns-apps > .gns-apps-separator {
        display: none;
        height: 0;
        /* 5px, not 3px: the rule is inset to the ICON TILE's edge, and the
         * tile now starts 5px in (the row's 2px padding + the icon's 3px
         * margin) in both rail states. */
        margin: 8px 5px;
        padding: 0;
        border-top: 2px solid color-mix(in srgb, var(--gns-ink) 10%, transparent);
        pointer-events: none;
    }

    /* Close an open, unpinned app from the EXPANDED rail.
     *
     * Those rows are on the rail only because their app is open, so closing is
     * the one thing you might want from them that the row itself doesn't do —
     * and it takes the row away, which is what makes the × honest here (a
     * pinned row stays put, so it gets no × at all).
     *
     * Different shape from the collapsed rail's close, which swaps the whole
     * tile: here the row keeps its icon and its label, and the × sits at the
     * right end as its own control. The HIT AREA is a square the height of the
     * row, flush to its right edge; the PLATE that appears under the × on
     * hover is inset well inside that — 32 square against the row's 44, more
     * tightly rounded — so it reads as nested inside rather than boxed against
     * it. It appears on row hover, taking the unread counter's place —
     * they share that end of the row. */
    body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item {
        position: relative;
    }
    body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item .gns-link.gns-close {
        display: flex;
        align-items: center;
        justify-content: center;
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        width: 44px;
        height: auto;
        padding: 0;
        background: none;
        opacity: 0;
        transition: opacity var(--gns-transition);
    }
    /* The × and nothing else. The control renders the shared app-icon partial,
     * which carries the app ICON and its LABEL as well — left up, the label
     * sat under the × clipped to its first letters ("Be✕" for Berichten). */
    body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item .gns-close .gns-icon:not(.gns-close-glyph),
    body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item .gns-close .gns-label {
        display: none;
    }
    body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item .gns-close .gns-close-glyph {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 32px;
        height: 32px;
        margin: 0;
        padding: 0;
        border-radius: 10px;
        background-color: transparent;
        /* Dim through the COLOUR, not opacity — opacity would fade the plate
         * behind it along with the glyph. */
        color: color-mix(in srgb, var(--gns-ink) 45%, transparent);
        opacity: 1;
        transition: background-color var(--gns-transition), color var(--gns-transition);
    }
    @media (hover: hover) {
        body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item:hover .gns-link.gns-close {
            opacity: 1;
        }
        /* Keep the ROW washed while the pointer is on the ×. The wash lives on
         * .gns-link:hover, and the close control is a SIBLING anchor laid over
         * the launcher — so reaching for the × took the pointer off the
         * launcher and the row went cold underneath the very control it was
         * offering. Paint it from the row's own hover instead. (Not for the
         * active row: its state shows through the coloured icon tile, never a
         * wash — same exception the rule below this one makes.) */
        body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item:not(.current):hover .gns-link.gns-launcher {
            background: color-mix(in srgb, var(--gns-ink) 5%, transparent);
        }
        /* The × takes the COUNTER's place — they occupy the same end of the
         * row, so with the badge left up the two overlapped into an unreadable
         * knot (an orange 1 with a × through it). Hiding the badge for the
         * duration of the hover also says the right thing: while you are
         * reaching for close, how many unread there are is not the point. */
        body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item:hover .gns-link.gns-launcher .gns-badge {
            display: none;
        }
        body[data-global-nav="side"]:not(.gns-collapsed):not(:has(#app-space.state-on)) #global-nav-side .gns-apps > .gns-item.gns-open-item .gns-link.gns-close:hover .gns-close-glyph {
            background-color: color-mix(in srgb, var(--gns-ink) 8%, transparent);
            color: color-mix(in srgb, var(--gns-ink) 85%, transparent);
        }
    }
}

/* One nav entry — workspace-tabs row metrics: 50px rows, 42px icon
 * block, bold label */
#global-nav-side .gns-link {
    display: flex;
    align-items: center;
    gap: 4px;
    /* 2px on the left in BOTH rail states, which is what the collapsed rail
     * needs to centre a 44px slot in its 48px content box. The expanded rail
     * doesn't need it for anything — it is here so the icon does not move at
     * all between the two states. Carried on the row rather than the icon so
     * the hover wash, which paints the padding box, keeps the full row width. */
    padding: 0 10px 0 2px;
    height: 44px;
    border-radius: 12px;
    box-sizing: border-box;
    color: #6B6560;
    font-weight: bold;
    text-decoration: none;
    white-space: nowrap;
    cursor: default;
    position: relative;
    transition: all var(--gns-transition);
}
@media (hover: hover) {
  #global-nav-side .gns-link:hover {
      background: color-mix(in srgb, var(--gns-ink) 5%, transparent);
  }
}

/* The active item's link has no background at all — its state shows
 * through the coloured icon tile and accent label, not a row wash */
@media (hover: hover) {
  #global-nav-side .gns-item.current > .gns-link:hover,
  #global-nav-side .gns-link[aria-current="page"]:hover {
      background: transparent;
  }
  /* …except a SPACE row, which keeps the wash even when it is the open one.
   * An app's active state is carried by a filled tile across the whole row's
   * height, so a wash on top adds nothing; a space's is a 38px filled circle
   * with a long row beside it, and dropping the wash left the open space as
   * the one row on the rail that didn't react to the pointer at all. In the
   * collapsed rail the rail-wide "no wash" rule still wins (2 ids) and the
   * hover square takes over. */
  #global-nav-side .gns-item.gns-space.current > .gns-link:hover {
      background: color-mix(in srgb, var(--gns-ink) 5%, transparent);
  }
}
#global-nav-side .gns-item.current > .gns-link,
#global-nav-side .gns-link[aria-current="page"] {
    color: var(--gns-accent);
    font-weight: 600;
}
#global-nav-side .gns-icon {
    flex: 0 0 auto;
    width: 38px;
    height: 38px;
    margin: 3px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
#global-nav-side .gns-icon svg {
    width: 100%;
    height: 100%;
}
/* App icons: borderless transparent tiles, every SVG shape painted in
 * the muted ink (#6B6560), matching the label colour. !important beats
 * fill attributes baked into the source SVGs. */
#global-nav-side .gns-icon.app-icon {
    border: none;
    box-shadow: none;
    border-radius: 10px;
    background-color: transparent;
    box-sizing: border-box;
    width: 38px;
    height: 38px;
    padding: 7px;
    overflow: hidden;
    transition: all var(--gns-transition);
}
#global-nav-side .gns-icon.app-icon * {
    fill: #6B6560 !important;
}
#global-nav-side .gns-icon.app-icon svg #dots {
    fill: transparent !important;
}
/* Active app: icon tile fills with the app's own colour, glyph in
 * white — the 42px rounded square the workspace tabs paint behind
 * their active icon */
#global-nav-side .gns-item.current .gns-icon.app-icon,
#global-nav-side .gns-link[aria-current="page"] .gns-icon.app-icon {
    background-color: var(--app-colour, var(--gns-accent));
}
#global-nav-side .gns-item.current .gns-icon.app-icon *,
#global-nav-side .gns-link[aria-current="page"] .gns-icon.app-icon * {
    fill: #fff !important;
}
#global-nav-side .gns-item.current .gns-icon.app-icon svg #dots,
#global-nav-side .gns-link[aria-current="page"] .gns-icon.app-icon svg #dots {
    fill: transparent !important;
}
/* Rail line icons (.rail-icon — an app icon the is_stroke_icon filter
 * sniffed as an outline; there is no separate rail-icons set): the
 * glyph is an outline, so the lines are COLOURED via stroke and the fill
 * stays empty. Each rule mirrors the filled-icon rule above at equal
 * specificity, later in source, so it wins for .rail-icon elements while
 * leaving fallback (filled) app icons on their fill rules. */
#global-nav-side .gns-icon.rail-icon * {
    fill: none !important;
    stroke: #6B6560 !important;
}
#global-nav-side .gns-icon.rail-icon svg #dots {
    fill: transparent !important;
    stroke: transparent !important;
}
/* Active app, line icons: the glyph rides the app-colour tile (from the
 * .app-icon rule above) with a white outline — the coloured-outline
 * treatment is reserved for the tooltip menu. */
#global-nav-side .gns-item.current .gns-icon.rail-icon *,
#global-nav-side .gns-link[aria-current="page"] .gns-icon.rail-icon * {
    fill: none !important;
    stroke: #fff !important;
}
#global-nav-side .gns-label {
    flex: 1 1 auto;
    max-width: 12rem;
    overflow: hidden;
    text-overflow: ellipsis;
    /* flex-grow is NOT transitioned — animating it forced a layout pass
     * per frame on every row; it snaps while the fade carries. The label
     * collapses via max-width (a cheap clip) rather than zoom — zoom
     * re-rasterised the label text every frame and was the main collapse
     * stutter on a rail full of rows. */
    transition: all var(--gns-transition);
}
@media only screen and (min-width: calc(768px + 1px)) {
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-label {
        flex-grow: 0;
        max-width: 0;
        opacity: 0;
    }
    /* The icon stays LEFT-aligned and the base row's 2px left padding does the
     * centring — it used to be justify-content: center, and that is not an
     * animatable property. It applied on the first frame of the collapse,
     * while the row was still 228px wide, so every icon was flung towards the
     * middle of a row that was about to disappear; the label's max-width then
     * snapped to 0 halfway through and threw them somewhere else again, and
     * the width animation walked them back. That three-step detour is the
     * wobble.
     *
     * Left-aligned there is no detour, and nothing here changes the icon's
     * position at all: every rail slot (38px icon + 3px margins, 26px space
     * letter + 9px margins) is 44px wide, so the row's own 2px + 44px + 2px
     * fills the collapsed rail's 48px content box and centres the slot to the
     * pixel — the same 2px the expanded rail already carries. Only the label's
     * trailing 10px goes, and there is no label here to push. */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-link {
        padding: 0 0 0 2px;
        gap: 0;
    }
    
    /* Collapsed rail: hover feedback moves from the row to the icon
     * tile itself (active icons keep their app-colour fill) */
    @media (hover: hover) {
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-link:hover {
          background: transparent;
      }
    }
    @media (hover: hover) {
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-item:not(.current) .gns-link:hover .gns-icon.app-icon {
          background-color: color-mix(in srgb, var(--gns-ink) 7%, transparent);
      }
    }
    /* Collapsed rail: glyphs go full black (the active app keeps its
     * white-on-app-colour tile, untouched by the :not(.current) scope) */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-item:not(.current) .gns-link:not([aria-current="page"]) .gns-icon.app-icon * {
        fill: #000 !important;
    }
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-item:not(.current) .gns-link:not([aria-current="page"]) .gns-icon.app-icon svg #dots {
        fill: transparent !important;
    }
    /* Rail line icons: colour the stroke black instead of the fill */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-item:not(.current) .gns-link:not([aria-current="page"]) .gns-icon.rail-icon * {
        fill: none !important;
        stroke: #000 !important;
    }
}

/* Team-space avatar — the space's first letter on its own colour
 * (--gns-space-colour, set inline per entry; accent fallback).
 * Inactive: transparent, a small outlined circle (see below).
 * Active workspace: solid colour, white letter, full 38px. */
#global-nav-side .gns-space-avatar {
    /* The ink themes pin --app-icon-contrast-colour (black) so space
     * avatars join the ink palette; other brands fall through to the
     * per-space colour. */
    background: color-mix(in srgb, var(--app-icon-contrast-colour, var(--gns-space-colour, var(--gns-accent))) 10%, white);
    color: var(--app-icon-contrast-colour, var(--gns-space-colour, var(--gns-accent)));
    border: 1px solid var(--app-icon-contrast-colour, var(--gns-space-colour, var(--gns-accent)));
    box-sizing: border-box;
    /* The space's letter mark stays a perfect circle — the CONTAINER around
     * it is what matches the apps (the hover tile is the app tile's 38px /
     * 10px square), not the mark itself. */
    border-radius: 50%;
    font-size: 0.75em;
    font-weight: 700;
    line-height: 1;
}

/* A space you are NOT in reads as a small outlined circle: the ring shrinks
 * and empties out, while the LETTER keeps its size — that is what you scan
 * the list by, and shrinking it too would just make the rail harder to read.
 * The open space grows back into the filled 38px avatar below, so the one
 * space you're in is the only weight in the section.
 *
 * The slot is unchanged in both states — box + margin is 44px either way —
 * so the labels never shift as the circle grows and shrinks, and the two
 * states can simply animate into each other. */
#global-nav-side .gns-item.gns-space .gns-space-avatar {
    width: 26px;
    height: 26px;
    margin: 9px;
    background: transparent;
    /* The hover tile is an ::after at z-index -1 (it has to sit behind the
     * letter). Without a stacking context of its own here, that negative
     * layer sinks past every ancestor to the rail's own background and never
     * shows — which is exactly what happened. isolate keeps it in this box. */
    position: relative;
    isolation: isolate;
    transition:
        width var(--gns-transition),
        height var(--gns-transition),
        margin var(--gns-transition),
        background-color var(--gns-transition),
        color var(--gns-transition);
}
/* The current workspace stays remembered (li.current) but only reads
 * as highlighted while the Workspaces pane is in the foreground — on
 * Home or in any other app it renders like the rest. */
#global-nav-side .gns-item.gns-space.current > .gns-link {
    color: #6B6560;
    font-weight: bold;
}
body:has(#app-space.state-on:is(.active-app-workspaces, .active-app-team-spaces)) #global-nav-side .gns-item.gns-space.current > .gns-link {
    color: var(--gns-accent);
    font-weight: 600;
}
/* HOVER: exactly what an app row does, in both rail states.
 *
 * EXPANDED — the whole row highlights, from the shared ``.gns-link:hover``
 * wash; nothing here overrides it.
 *
 * COLLAPSED (or any app open) — the row wash is off rail-wide and the feedback
 * moves to the icon: a rounded-corner square behind the mark at the app tile's
 * own geometry (38px, 10px radius, 7% ink). Painted as an ::after rather than
 * a background on the avatar, because the mark is a smaller circle sitting in
 * the middle of that slot; ``inset: -6px`` on the 26px circle lands exactly on
 * the app tile's footprint. */
@media (hover: hover) {
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-item.gns-space .gns-link:hover .gns-space-avatar::after {
        content: "";
        position: absolute;
        inset: -6px;
        border-radius: 10px;
        background-color: color-mix(in srgb, var(--gns-ink) 7%, transparent);
        z-index: -1;
    }
    /* The open space is the ONE row that skips the square — its avatar is
     * already filled, and a wash behind a filled tile just muddies it. But
     * only while the space is actually in the foreground: an open space with
     * another app on top keeps its ``current`` class and renders like any
     * other row (same gate the accent-label rule uses), so with the row wash
     * off in this state it had no hover feedback left at all. */
    body:has(#app-space.state-on:is(.active-app-workspaces, .active-app-team-spaces)) #global-nav-side .gns-item.gns-space.current .gns-link:hover .gns-space-avatar::after {
        display: none;
    }
}

/* The OPEN space: the full-size filled avatar — the state every space used
 * to render in, now reserved for the one you're actually looking at. */
body:has(#app-space.state-on:is(.active-app-workspaces, .active-app-team-spaces)) #global-nav-side .gns-item.gns-space.current .gns-space-avatar {
    width: 38px;
    height: 38px;
    margin: 3px;
    background: var(--app-icon-contrast-colour, var(--gns-space-colour, var(--gns-accent)));
    color: #fff;
}

/* The "+" entry above the space list (opens the New team space modal):
 * a dashed neutral tile so it reads as an action, not another space. */
#global-nav-side .gns-space-add .gns-space-avatar {
    background: transparent;
    border-style: dashed;
    border-color: #6B6560;
    color: #6B6560;
    font-size: 1.1em;
    font-weight: 600;
}
@media (hover: hover) {
  #global-nav-side .gns-space-add .gns-link:hover .gns-space-avatar {
      border-color: var(--gns-ink);
      color: var(--gns-ink);
  }
}

/* Too many spaces to fit without scrolling: KikaronGlobalNav flags the
 * rail with .gns-spaces-overflow and the per-space list collapses into
 * the single Team spaces app icon (the .gns-spaces-all entry).
 *
 * UNLESS Team spaces is PINNED. A pinned Team spaces already has its own
 * icon up among the apps (global-nav-mobile-order.css keys that off the same
 * data-gns-pinned marker), so collapsing would trade every direct space entry
 * for a SECOND copy of an icon the rail is already showing — losing the
 * spaces and gaining nothing. Pinned, the list stays and scrolls instead. */
#global-nav-side .gns-spaces .gns-spaces-all {
    display: none;
}
#global-nav-side.gns-spaces-overflow:not(:has(.gns-apps .gns-app-team-spaces [data-gns-pinned])) .gns-spaces .gns-spaces-all {
    display: block;
}
#global-nav-side.gns-spaces-overflow:not(:has(.gns-apps .gns-app-team-spaces [data-gns-pinned])) .gns-spaces > ul:not(.gns-spaces-all),
#global-nav-side.gns-spaces-overflow:not(:has(.gns-apps .gns-app-team-spaces [data-gns-pinned])) .gns-spaces .gns-section-title {
    display: none;
}

/* Unread badge — keeps working in the collapsed rail (rides the icon) */
#global-nav-side .gns-badge {
    flex: 0 0 auto;
    background: var(--gns-accent);
    color: #fff;
    border-radius: 999px;
    font-size: 0.7em;
    line-height: 1;
    padding: 3px 6px;
    top: 0;
}
@media only screen and (min-width: calc(768px + 1px)) {
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-badge {
        position: absolute;
        top: 4px;
        right: 2px;
    }
}

/* Collapsed rail: labels become tooltips (CSS-only, from data-gns-label) */
@media only screen and (min-width: calc(768px + 1px)) {
    @media (hover: hover) {
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-link[data-gns-label]:hover::after,
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-hub[data-gns-label]:hover::after,
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-logo[data-gns-label]:hover::after {
          content: attr(data-gns-label);
          position: fixed;
          left: calc(var(--gns-left) + var(--gns-width-collapsed) + 6px);
          background: var(--gns-background);
          color: var(--gns-ink);
          font-size: 0.8em;
          font-weight: 400;
          padding: 4px 10px;
          border-radius: 4px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
          white-space: nowrap;
          z-index: 1000;
          pointer-events: none;
      }
      /* A PINNED app says so in its tooltip — the row itself keeps showing the
       * app icon at every state, hover included. (An earlier take swapped the
       * icon for a pin glyph on hover; it cost you the icon for a fact that
       * isn't about what the click does.) The marker rides the tooltip's
       * ``content`` rather than data-gns-label so the attribute stays the plain
       * app name. A literal pushpin character, not the fontello glyph: the
       * pseudo-element renders the label in the UI font, and one content string
       * can only have one font-family. */
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-link[data-gns-label][data-gns-pinned]:hover::after {
          content: "\1F4CC\00a0" attr(data-gns-label);
      }
    }

    /* The platform logo is a tall 67px block, so the fixed pseudo's static
     * vertical position falls to the block's bottom — well below the small
     * 38px collapsed brandmark tile. Anchor it to the rail top instead so it
     * centres on the swirl tile (rail padding-top 22px + half the 38px tile). */
    @media (hover: hover) {
      body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-logo[data-gns-label]:hover::after {
          top: calc(var(--gns-top) + 30px);
      }
    }
}

/* ------------------------------------------------------------------ */
/* Chrome swap: what Side mode does to the rest of the page            */
/* ------------------------------------------------------------------ */

/* The header is gone in Side mode, so every layout offset derived
 * from its height collapses to zero (0px, not 0 — unitless zero is
 * invalid inside calc() additions). The dashboard's top margin made
 * room under that header, so it goes too. */
body[data-global-nav="side"] {
    --global-header-height: 0px;
    --dashboard-margin-top: 0px;
}
/* The dashboard sits flush to the top in Side mode — no header to
 * clear. dashboard.css declares --dashboard-margin-top ON
 * .quaive-dashboard itself at large widths (80px, or 40px via
 * #portal:not(:has(#portal-tabs))), which out-ranks the inherited
 * body-level reset, so re-zero it on the element with matching/higher
 * specificity. The second selector beats the (2,1,0) 40px rule;
 * together they cover every width. */
body[data-global-nav="side"] #portal .quaive-dashboard,
body[data-global-nav="side"] #portal:not(:has(#portal-tabs)) .quaive-dashboard {
    --dashboard-margin-top: 0px;
}

/* Side-mode dashboard goes full-bleed: drop the 1280px container cap so
 * the portlet grid uses the whole canvas (the apps/favourites portal
 * sections keep their cap — this is scoped to .section-dashboard). The
 * content box also becomes a size container (gns-dash) so the rules below
 * react to the REAL available width, not the viewport — the rail inset
 * varies (expanded on Home, the 50px strip in tablet-portrait), so a
 * viewport breakpoint would mis-judge how many columns actually fit. */
body[data-global-nav="side"] #portal.section-dashboard #portal-content {
    max-width: none;
    --pat-container-width: none;
    container: gns-dash / inline-size;
}
/* Stop capping the column count: instead of a fixed 2fr/3-track template,
 * auto-fill spawns one more ~320px lane every time another fits, so the
 * portlet grid keeps gaining columns as the canvas widens. */
body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard {
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    grid-auto-flow: row dense;
}
/* The Apps portlet used to be hidden here as redundant (the rail lists the
 * apps). It is back, directly after the Chokhmah/zmanim portlet — the rail's
 * own Apps entry is a candy-box launcher, not the grid — so it is now an
 * ordinary span-1 portlet: shown from size 3 on, hidden at sizes 1–2 by their
 * essentials filter like every other portlet. Its lane pin sits with zmanim's
 * at the bottom of this section. */
/* The activity stream always lives in the first column — pin its column
 * line to 1 so the masonry/auto-flow algorithm can't drift it into
 * another lane (it would otherwise seek the shortest one). The base
 * span-every-row pin is dropped (no fixed track count to span). */
body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet#portlet-updates {
    grid-column: 1;
    grid-row: auto;
    align-self: start;
}
/* Native masonry where the browser has it: portlets pack up into the
 * shortest lane rather than aligning to row tracks, removing the ragged
 * vertical gaps a plain auto-flow grid leaves. Falls back to the grid
 * above wherever display: grid-lanes is unsupported. */
@supports (display: grid-lanes) {
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard {
        display: grid-lanes;
    }
}
/* The lane width (320px) + gutter (20px) sets the breakpoints:
 *   1 lane  : <660px    (size 1, "mobile")
 *   2 lanes : 660–999   (size 2)
 *   3 lanes : 1000–1339 (size 3) …and +340px per lane after that.
 *
 * Two lanes or more: the activity stream is the prominent wide column,
 * spanning the first two lanes. In size 2 that makes it full-width; from
 * size 3 on it's the wide left column with span-1 portlets to its right
 * — and it always starts at lane 1 ("stays in the first column"). */
@container gns-dash (width >= 660px) {
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet#portlet-updates {
        grid-column: 1 / span 2;
    }
}

/* Size 1 (one lane, "mobile"): only the activity stream. Everything else,
 * Zmanim and News included, is hidden — and the lone activities portlet
 * sheds its card chrome to read as the bare feed (full-width column, no
 * title, no panel background/shadow/border/padding). Container-query
 * based, so it holds whenever the dashboard column is this narrow — the
 * mobile bottom-bar layout AND a desktop side-nav with the rail expanded. */
@container gns-dash (width < 660px) {
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet:not(#portlet-updates) {
        display: none;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard {
        grid-template-columns: 1fr;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet {
        grid-column: 1 / -1;
        background: transparent;
        box-shadow: none;
    }
    body[data-global-nav="side"] #portlet-updates .well-title-group,
    body[data-global-nav="side"] #portlet-activity-stream .well-title-group {
        display: none;
    }
    /* No card chrome and NO padding, on the portlet or its body: with nothing
     * above it the stream IS the page, so it starts at the top edge.
     *
     * Qualified through the dashboard (#portal.section-dashboard
     * .quaive-dashboard) only to outrank the ``padding-top: 20px`` in the
     * ≥769px MEDIA block further down. That one is a VIEWPORT query and this
     * is a CONTAINER query, so on a narrow tablet portrait — viewport ≥769,
     * dashboard column <660 because the expanded rail eats the width — both
     * matched at equal specificity and the later one won, dropping a 20px gap
     * above a feed that has nothing to be spaced from. */
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-updates,
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-activity-stream {
        padding: 0;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-updates .panel-content,
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-activity-stream .panel-content {
        background: transparent;
        box-shadow: none;
        border: none;
        padding: 0;
    }
    /* The standalone compose + comment boxes carry ``margin: 2rem auto`` from
     * base.css so they get breathing room on a roomy board. Here the card
     * padding around them is gone, so that fixed 2rem is measured from the
     * screen edge instead of from a card, and the `auto` centring has nothing
     * left to centre against. Percentages instead: the inset scales with the
     * column, which at this width IS the screen, so the box keeps the same
     * proportion of air around it from a narrow phone to a tablet portrait —
     * where a fixed rem is generous on one and cramped on the other. */
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard .update-social.post-box:not(.post *),
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard .update-social.comment-box:not(.post *) {
        margin: 5% 5% 3%;
        max-width: 100%;
    }
}

/* Mobile (the bottom-bar breakpoint): the stream and nothing above it.
 *
 * A VIEWPORT query, deliberately, where the tiers below are container queries.
 * With the rail dissolved into a bottom bar the dashboard column gets the whole
 * screen, so a phone in landscape measures wide enough to land in the two-lane
 * tier and grew a card above the feed. On a phone the feed is the page whatever
 * the column happens to measure. */
@media only screen and (max-width: 768px) {
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet:not(#portlet-updates):not(#portlet-activity-stream) {
        display: none;
    }
    /* …and the same chrome the one-lane tier sheds. Deliberately repeated
     * rather than shared: the tier is a CONTAINER query and this is a VIEWPORT
     * one, and CSS has no way to say "either of these". Without it a phone whose
     * dashboard column happens to measure ≥660 — the bar gives the board the
     * whole screen — got the mobile bottom bar wearing the tablet's card: a
     * well title, a panel background and padding around a feed that is the
     * whole page. Keep the two blocks in step. */
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard {
        grid-template-columns: 1fr;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet {
        grid-column: 1 / -1;
        background: transparent;
        box-shadow: none;
    }
    body[data-global-nav="side"] #portlet-updates .well-title-group,
    body[data-global-nav="side"] #portlet-activity-stream .well-title-group {
        display: none;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-updates,
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-activity-stream {
        padding: 0;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-updates .panel-content,
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard #portlet-activity-stream .panel-content {
        background: transparent;
        box-shadow: none;
        border: none;
        padding: 0;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard .update-social.post-box:not(.post *),
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard .update-social.comment-box:not(.post *) {
        margin: 5% 5% 3%;
        max-width: 100%;
    }
}

/* Size 2a (two lanes, but only just): the LAUNCHER and the activity stream,
 * nothing else. Two cards do fit side by side from 660px, but not these two —
 * Zmanim wants the width for its Hebrew date and Shabbat times, and the lane
 * left for Apps collapsed to a sliver of a card. So at the narrow end of the
 * two-lane range only one card sits above the stream, and it is the launcher:
 * a squeezed grid of app tiles is useless, while the same grid across the full
 * width is the most useful thing on a small board. Zmanim rejoins at 860px,
 * where both lanes are ~420px and neither has to give anything up. */
@container gns-dash (660px <= width < 860px) {
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard {
        display: grid;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet:not(#portlet-apps, #portlet-updates) {
        display: none;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-apps {
        grid-column: 1 / span 2;
        order: 1;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-updates {
        order: 2;
    }
}

/* Size 2b (two roomy lanes): Zmanim, Apps and the activity stream. Zmanim and
 * Apps sit on the top row side by side (one lane each); the activity stream
 * spans both lanes underneath (its span comes from the ≥660 rule above, so it
 * drops to the next row). `order` fixes the stack: Zmanim, Apps, then
 * Activities.
 *
 * Apps rather than News here: at this width you get three cards, and the one
 * that earns a slot is the one that takes you somewhere — the launcher for
 * everything the rail doesn't already pin. News is a read, and the activity
 * stream below it is already the reading surface. */
@container gns-dash (860px <= width < 1000px) {
    /* Plain grid here, never grid-lanes: this tier relies on `order` +
     * row placement to stack Zmanim/Apps over the spanning activity
     * stream, which the masonry algorithm wouldn't honour. Overrides the
     * @supports (display: grid-lanes) rule above (same selector, later in
     * source). */
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard {
        display: grid;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet:not(#portlet-zmanim, #portlet-apps, #portlet-updates) {
        display: none;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-zmanim {
        order: 1;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-apps {
        order: 2;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-updates {
        order: 3;
    }
    /* No Zmanim (Jewish add-on off): Apps takes the full top row, the
     * activity stream below it. */
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard:not(:has(#portlet-zmanim)) > #portlet-apps {
        grid-column: 1 / span 2;
        order: 1;
    }
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard:not(:has(#portlet-zmanim)) > #portlet-updates {
        order: 2;
    }
}

/* Size 3 and up (three+ lanes): every portlet is back. The activity
 * stream holds the first two lanes and the span-1 portlets fill the lanes
 * to its right, gaining one more column per +340px. Native masonry packs
 * those narrow portlets into the shortest lane on its own. The plain-grid
 * fallback can't — without help a row-grid aligns every lane to shared
 * row tracks, so the tall activity stream would blow open huge gaps in
 * the narrow lanes. The classic fix: make it span every row (1 / span
 * 100) so the span-1 portlets pack row-by-row down lanes 3+ instead. Only
 * needed where grid-lanes is absent. */
@supports not (display: grid-lanes) {
    @container gns-dash (width >= 1000px) {
        body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > .quaive-portlet#portlet-updates {
            grid-row: 1 / span 100;
        }
    }
}
/* From size 3 on, Zmanim is a normal span-1 portlet — but it and Apps are
 * the only ones that precede the activity stream in source order, so under
 * masonry (which places in document order) they would otherwise settle into
 * lane 1 ABOVE the stream. Pin both into the second column (lane 3 — the
 * first narrow lane, since the stream spans lanes 1–2) so they sit beside
 * the stream, not above it; source order then stacks Apps directly under
 * Zmanim, which is where it belongs. Harmless in the plain-grid fallback,
 * where they already land there. */
@container gns-dash (width >= 1000px) {
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-zmanim,
    body[data-global-nav="side"] #portal.section-dashboard .quaive-dashboard > #portlet-apps {
        grid-column: 3;
    }
}

/* Favourites-as-app pane visibility — no App registry row means no
 * generated show-rule in automations/apps.css, so it lives here. */
#app-space.active-app-favourites.state-on #application-body-favourites {
    display: block;
}

/* App sidebars span the full app-body height — the header is gone, so
 * the height calc that subtracted it no longer applies. */
body[data-global-nav="side"] #app-space .sidebar.left {
    height: 100%;
}

/* Rail mode: paint the app sidebars with the light app colour — a pale wash of
 * the active app's hue — so they read as part of the app, not the chrome.
 * Overriding the shared --sidebar-background-colour recolours both the sidebar
 * and its content (both consume that variable). */
body[data-global-nav="side"] .application-body {
    --sidebar-background-colour: color-mix(in srgb, var(--app-colour, white) 15%, white);
}

/* Legacy top chrome disappears while Side is active */
body[data-global-nav="side"] #global-header,
body[data-global-nav="side"] #portal-tabs,
body[data-global-nav="side"] #tabs-bar {
    display: none !important;
}

/* base.html's default #portal-content lazy-loads the APPS PORTAL (grid + its
 * launcher toolbar) as the fallback landing, so on any full-page app load —
 * e.g. the redirect after saving language prefs — that portal would otherwise
 * flash in #portal behind the open app. Home in Side mode is the dashboard,
 * reached from the rail, so suppress it.
 *
 * Scoped to the apps portal's OWN grid (#apps-listing), not to
 * `.kik-apps-grid` at large: the dashboard's Apps portlet is a grid in #portal
 * too, and the blanket rule silently blanked it — a card with a header and no
 * tiles, with nothing in the markup to explain why. */
body[data-global-nav="side"] #portal #apps-listing,
body[data-global-nav="side"] #portal #app-launcher-toolbar {
    display: none !important;
}
/* The header drop-shadow #app-space paints along its top edge belongs
 * to the hidden header — without this it floats as a stray gradient. */
body[data-global-nav="side"] #app-space:not(.injecting)::before {
    opacity: 0 !important;
}

/* Content stays at left: 0 — the rail is superimposed on top of it,
 * exactly like the workspace-tabs column floats over workspace pages.
 * top: 0 because the hidden header no longer needs headroom. */
body[data-global-nav="side"] #app-space,
body[data-global-nav="side"] #portal {
    top: 0;
    margin-left: 0;
}

@media only screen and (min-width: calc(768px + 1px)) {
    /* The portal content clears the full rail */
    body[data-global-nav="side"] #portal {
        padding-left: calc(var(--gns-left) + var(--gns-width));
        box-sizing: border-box;
    }
    /* …but a collapsed rail (tablet-portrait Home, or any open app)
     * shrinks to the 50px icon strip — the portal reclaims that width
     * and only clears the strip, so it never sits narrow beside a gap
     * where the expanded rail used to be. Tracks the same trigger as the
     * rail-width rule above. */
    body[data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #portal {
        padding-left: calc(var(--gns-left) + var(--gns-width-collapsed));
    }
    /* Apps (collapsed rail): every app body adopts the workspaces-app
     * layout recipe — the sidebar's content padding and the document
     * body's left distance both reserve the 50px rail, exactly as
     * #application-body-workspaces does for its own tabs column. The
     * stock aside.sidebar/.document-body rules in all.css consume
     * these vars; nothing else is touched. */
    body[data-global-nav="side"] #app-space .application-body {
        /* same width the workspaces app gives its sidebar (475px vs the
         * 400px platform default) */
        --sidebar-width: 475px;
        /* re-declared HERE (like #application-body-workspaces does) so
         * the calc picks up the 475px width — the :root default already
         * resolved against 400px and inherits as that fixed value */
        --document-body-left-distance-sidebar-open: calc(var(--sidebar-width) + var(--sidebar-margin-left, 15px));
        --sidebar-padding:
            var(--sidebar-padding-top)
            var(--sidebar-padding-right)
            var(--sidebar-padding-bottom)
            calc(var(--gns-width-collapsed) + 1.5 * var(--sidebar-padding-left));
        --document-body-left-distance: calc(var(--gns-width-collapsed) + var(--sidebar-padding-left));
        /* Closed sidebars keep a visible strip under the rail, exactly
         * like the workspaces app: its -397px against a 475px sidebar
         * leaves ~78px = tabs column + inset. Here that strip is the
         * rail width + the rail's left inset, whatever the app's
         * sidebar width. */
        --sidebar-margin-left-closed: calc(var(--gns-width-collapsed) + var(--gns-left) - var(--sidebar-width, 400px));
    }
    /* Large sidebars get the bigger left padding the workspaces app
     * uses (#application-body-workspaces.sidebar-large:
     * --sidebar-padding-left-large = 85px + padding) so the content
     * clears the rail. */
    body[data-global-nav="side"] #app-space .application-body.sidebar-large {
        --sidebar-padding-left-large: calc(85px + var(--sidebar-padding-left, 25px));
    }
    /* Switching to an app with an open sidebar: the sidebar slides and
     * fades in from outside the left edge. CSS animations restart when
     * the app body flips from display:none to rendered (pane switch)
     * and when a fresh body is pat-injected, so every arrival animates. */
    body[data-global-nav="side"] #app-space .application-body.sidebar-left-open aside.sidebar.left {
        animation: gns-sidebar-slide-in var(--base-animation-duration, 0.3s) ease-out;
    }
    /* The visible strip of a closed sidebar is chrome, not content —
     * fade its contents out and let clicks fall through (the stock
     * .sidebar-content transition animates the fade). */

    body[data-global-nav="side"] aside.sidebar.left .sidebar-content {
        transition: all var(--gns-transition);
        
        > * {
            transition: all var(--gns-transition);
        }
    }

    body[data-global-nav="side"] #app-space .application-body.sidebar-left-closed aside.sidebar.left #sidebar-content,
    body[data-global-nav="side"] #app-space .application-body.sidebar-left-closed aside.sidebar.left .sidebar-content {
        /* opacity: 0; */
        pointer-events: none;

        > * {
            opacity: 0;
        }
    }
    /* Dashboard updates portlet: no well header (the rail/logo is the
     * navigation), just the feed with a little breathing room on top.
     * Covers the lean id (portlet-updates) and classic
     * (portlet-activity-stream). */
    body[data-global-nav="side"] #portlet-updates .well-title-group,
    body[data-global-nav="side"] #portlet-activity-stream .well-title-group {
        display: none;
    }
    body[data-global-nav="side"] #portlet-updates .panel-content,
    body[data-global-nav="side"] #portlet-activity-stream .panel-content {
        padding-top: 20px;
    }
    /* Messages Element iframe — same insets as the calendar canvas
     * below (its stock rule uses the 15px sidebar margins). */
    body[data-global-nav="side"] #app-space .application-body.application-messages .pat-element {
        top: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        right: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        bottom: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        left: var(--sidebar-padding-left, 25px);
    }
    /* Mail webmail iframe — same inset "card" box as the Messages Element
     * iframe above so the two full-bleed iframe apps line up in hub mode.
     * (Mail has no base absolute rule, so set position here too; the default
     * is the full-bleed width/height:100% from apps.css.) */
    body[data-global-nav="side"] #app-space .application-body.application-mail .mail-webmail-frame {
        position: absolute;
        top: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        right: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        bottom: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        left: var(--sidebar-padding-left, 25px);
        width: -webkit-fill-available;
        height: -webkit-fill-available;
        /* Same card chrome as the generic `.pat-element iframe` (workspace
         * chat / office editors): rounded corners + the standard drop shadow. */
        border-radius: var(--border-radii);
        box-shadow: var(--default-box-shadow);
    }
    /* Calendar canvas — same inset recipe as the workspace-chat
     * Element iframe (#application-body-workspaces .pat-element
     * iframe): 27.5px top/right/bottom, 25px left, card chrome. */
    body[data-global-nav="side"] #app-space .application-calendar .pat-calendar#calendar-app {
        position: absolute;
        top: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        right: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        bottom: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        left: var(--sidebar-padding-left, 25px);
        border-radius: var(--border-radii, 8px);
        box-shadow: var(--default-box-shadow);
    }
    /* The workspaces host reserves the SAME 50px for its own tabs
     * column — push its tabs right of the global rail and widen the
     * insets so both columns fit side by side. */
    body[data-global-nav="side"] #app-space #application-body-workspaces {
        --workspace-tabs-left: calc(var(--gns-left) + var(--gns-width-collapsed));
        --workspace-tabs-left-closed: calc(var(--gns-left) + var(--gns-width-collapsed));
        --sidebar-padding:
            var(--sidebar-padding-top)
            var(--sidebar-padding-right)
            var(--sidebar-padding-bottom)
            calc(2 * var(--gns-width-collapsed) + 1.5 * var(--sidebar-padding-left));
        --document-body-left-distance: calc(2 * var(--gns-width-collapsed) + var(--sidebar-padding-left));
    }
}

/* RTL (Hebrew/Arabic): rail rides the right edge; the var-driven
 * margins/distances are consumed by all.css's own RTL swaps, but the
 * physical insets declared above (padding shorthands, left/right
 * coordinates) need explicit mirrors. */
body[dir="rtl"] #global-nav-side {
    left: auto;
    right: var(--gns-left);
}
/* Header/logo: the 10px inline inset moves to the right edge (the
 * wordmark <img> right-aligns by itself — block replaced element under
 * direction: rtl) and the stacked brandmark anchors right too. */
body[dir="rtl"] #global-nav-side .gns-header {
    padding: 0 10px 10px 0;
}
body[dir="rtl"] #global-nav-side .gns-logo-mark {
    left: auto;
    right: 0;
}
/* Row insets: the 10px end-padding sits before the label, not the icon,
 * so the icons line up under the logo like they do in LTR. */
body[dir="rtl"] #global-nav-side .gns-link {
    padding: 0 0 0 10px;
}
@media only screen and (min-width: calc(768px + 1px)) {
    
    /* Collapsed-rail label tooltips flip to the rail's left side (match
     * the LTR selector breadth incl. the :has() collapse variant). */
    @media (hover: hover) {
      body[dir="rtl"][data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-link[data-gns-label]:hover::after,
      body[dir="rtl"][data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-hub[data-gns-label]:hover::after,
      body[dir="rtl"][data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-logo[data-gns-label]:hover::after {
          left: auto;
          right: calc(var(--gns-left) + var(--gns-width-collapsed) + 6px);
      }
    }
    /* Collapsed logo tile insets from the right edge; the brandmark
     * keeps its 5px inset measured from the right. */
    body[dir="rtl"][data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-platform-logo {
        margin-left: 0;
        margin-right: 6px;
    }
    body[dir="rtl"][data-global-nav="side"]:is(.gns-collapsed, :has(#app-space.state-on)) #global-nav-side .gns-logo-mark {
        left: auto;
        right: 5px;
    }
    /* The portal clears the rail on the right instead */
    body[dir="rtl"][data-global-nav="side"] #portal {
        padding-left: 0;
        padding-right: calc(var(--gns-left) + var(--gns-width));
    }
    /* The --sidebar-padding shorthand is physical (top right bottom
     * left): the rail inset moves from the left to the right component,
     * mirroring the workspaces-app RTL recipe in _sidebar.scss. */
    body[dir="rtl"][data-global-nav="side"] #app-space .application-body {
        --sidebar-padding:
            var(--sidebar-padding-top)
            calc(var(--gns-width-collapsed) + 1.5 * var(--sidebar-padding-left))
            var(--sidebar-padding-bottom)
            var(--sidebar-padding-right);
    }
    body[dir="rtl"][data-global-nav="side"] #app-space #application-body-workspaces {
        --sidebar-padding:
            var(--sidebar-padding-top)
            calc(2 * var(--gns-width-collapsed) + 1.5 * var(--sidebar-padding-left))
            var(--sidebar-padding-bottom)
            var(--sidebar-padding-right);
    }
    /* Open sidebars slide in from the right edge */
    body[dir="rtl"][data-global-nav="side"] #app-space .application-body.sidebar-left-open aside.sidebar.left {
        animation-name: gns-sidebar-slide-in-rtl;
    }
    /* Messages Element iframe + calendar canvas: mirror the wide rail
     * inset onto the right side. */
    body[dir="rtl"][data-global-nav="side"] #app-space .application-body.application-messages .pat-element,
    body[dir="rtl"][data-global-nav="side"] #app-space .application-calendar .pat-calendar#calendar-app {
        left: calc(var(--sidebar-margin-top, 15px) + 0.5 * var(--sidebar-padding-top, 25px));
        right: var(--sidebar-padding-left, 25px);
    }
}

/* The mobile bottom bar's leading Home button (the brandmark) — the
 * desktop rail uses the .gns-header logo instead, so hide it here. */
#global-nav-side .gns-mobile-home {
    display: none;
}

/* "More apps" overflow entry — hidden by default, and on the DESKTOP rail it
 * now stays that way: the Apps launcher has its own rail entry and lists every
 * app, so global-nav.js no longer sets .gns-apps-overflow there — it just
 * marks what didn't fit .gns-overflowed and lets it drop off. Only the mobile
 * bar still reveals a More entry, via the breakpoint rules further down. */
#global-nav-side .gns-more-item {
    display: none;
}
#global-nav-side.gns-apps-overflow .gns-more-item {
    display: block;
}
#global-nav-side .gns-apps .gns-item.gns-overflowed {
    display: none;
}
/* In the popover, show only what the rail dropped. The mobile bar drives the
 * same popover through its nth-child mirrors, so this desktop-only marker
 * applies above the mobile breakpoint. */
@media (min-width: calc(768px + 1px)) {
    #global-nav-side .gns-more-popover .gns-more-app.gns-more-app-railed {
        display: none;
    }
    /* …and dock the popover beside the RAIL. The base rules park it over the
     * mobile bar (fixed to the bottom, spanning the viewport width), which is
     * nowhere near the desktop trigger. Cap the height and let the popover
     * itself scroll: it is the overflow surface, so it may legitimately be
     * longer than the rail that couldn't hold its contents. */
    body[data-global-nav="side"] .gns-more-popover {
        /* rail's right edge + a 15px gap */
        left: calc(var(--gns-width-collapsed, 64px) + var(--gns-left, 15px) + 15px);
        right: auto;
        bottom: calc(var(--gns-left, 15px) + 48px);
        width: min(280px, calc(100vw - 120px));
        max-height: 70vh;
        overflow-y: auto;
    }
    /* Apps only on tablet + desktop. The popover carries a mirror of the
     * personal menu (org switcher, personal cloud, Preferences, Log out)
     * because on PHONES it doubles as the user menu — the avatar trigger is
     * hidden at that width. Above it the avatar is back in the header, so
     * those entries would be a second copy; here the popover is purely the
     * rail's app overflow. */
    body[data-global-nav="side"] .gns-more-popover > ul.menu {
        display: none;
    }
    /* One column, not two. The two-track grid is a phone affordance — the
     * popover spans the viewport there, so pairing the apps keeps it short.
     * Beside the rail it is a narrow panel, so a single column reads as a
     * plain list and the labels stop competing for half-width tracks. */
    body[data-global-nav="side"] .gns-more-popover .gns-more-list {
        grid-template-columns: minmax(0, 1fr);
    }
}
/* The trigger is a <button popovertarget> — strip native chrome so it
 * reads as a plain rail icon; default cursor (it's a control). */
#global-nav-side .gns-more-trigger {
    background: none;
    border: none;
    /* Strip the UA button chrome WITHOUT overriding the rail's own link
     * treatment. `font: inherit` is a shorthand — it reset font-weight to the
     * inherited 400 — and `color: inherit` took the row's colour, so "More"
     * rendered lighter and darker than every app name beside it. Family and
     * size still need inheriting (a UA button defaults to a small system
     * font); colour and weight are left to the .gns-link rules this button
     * also matches, so the entry reads exactly like an app name. */
    font-family: inherit;
    font-size: inherit;
    cursor: default;
    width: 100%;
}
/* While the overflow popover is open, the trigger reads as active: the tile
 * stays transparent and the glyph is painted in the accent colour (fill for
 * filled icons, stroke for line icons). The popover is a DOM descendant of
 * the rail, so :has() matches from #global-nav-side; the ID prefix outranks
 * the rail's base `.gns-icon.app-icon`/`.rail-icon *` rules. */
#global-nav-side:has(.gns-more-popover:popover-open) .gns-more-trigger .gns-icon {
    background-color: transparent;
}
#global-nav-side:has(.gns-more-popover:popover-open) .gns-more-trigger .gns-icon.app-icon * {
    fill: var(--colour-accent) !important;
}
#global-nav-side:has(.gns-more-popover:popover-open) .gns-more-trigger .gns-icon.rail-icon * {
    fill: none !important;
    stroke: var(--colour-accent) !important;
}

/* The overflow popover (HTML Popover API) — a floating app launcher above
 * the bar. UA styles centre popovers in the viewport; override the inset/
 * margin to dock it just over the bar, spanning the bar's width. */
.gns-more-popover {
    position: fixed;
    inset: auto;
    margin: 0;
    bottom: calc(
        var(--workspace-tabs-bar-height, var(--gns-mobile-height))
        + 0.5 * var(--small-screen-padding, 10px)
        + env(safe-area-inset-bottom, 0px)
        + 10px
    );
    left: calc(0.5 * var(--small-screen-padding, 10px));
    right: calc(0.5 * var(--small-screen-padding, 10px));
    width: auto;
    max-width: none;
    box-sizing: border-box;
    padding: 16px;
    border: none;
    border-radius: var(--border-radii, 8px);
    background-color: var(--pat-surface-background-colour, rgba(255, 255, 255, 0.9));
    -webkit-backdrop-filter: blur(10px) saturate(4) brightness(1.2);
    backdrop-filter: blur(10px) saturate(4) brightness(1.2);
    box-shadow: var(--pat-toolbar-box-shadow, var(--default-box-shadow));
    color: var(--gns-ink);
    /* Fade + slide in; allow-discrete animates the display/overlay flips */
    opacity: 0;
    transform: translateY(8px);
    transition:
        opacity var(--gns-transition),
        transform var(--gns-transition),
        overlay var(--base-animation-duration, 0.2s) allow-discrete,
        display var(--base-animation-duration, 0.2s) allow-discrete;
}
.gns-more-popover:popover-open {
    opacity: 1;
    transform: translateY(0);
}
@starting-style {
    .gns-more-popover:popover-open {
        opacity: 0;
        transform: translateY(8px);
    }
}
/* The mirrored personal menus stack above the app list; space each block
 * off from the next (and the app list below). */
#global-nav-side .gns-more-popover ul.menu {
    margin-bottom: 15px;
}
.gns-more-popover .gns-more-list {
    list-style: none;
    margin: 8px 0px;
    padding: 0;
    display: grid;
    /* minmax(0, 1fr) — not the default minmax(auto, 1fr) — so the nowrap app
     * labels can't blow the tracks wider than their half of the popover and
     * trigger a horizontal scrollbar; the labels ellipsize instead. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 2px;
    max-height: 60vh;
    overflow-x: hidden;
    overflow-y: auto;
}
.gns-more-popover .gns-more-link {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 5px;
    border-radius: 8px;
    text-decoration: none;
    color: var(--gns-ink);
    white-space: nowrap;
    overflow: hidden;
}
@media (hover: hover) {
  .gns-more-popover .gns-more-link:hover {
      background: color-mix(in srgb, var(--gns-ink) 6%, transparent);
  }
}
/* #global-nav-side prefix so these outrank the rail's ID-scoped
 * `#global-nav-side .gns-icon.app-icon` base rules — the popover lives
 * inside the rail, so a class-only selector loses to them. */
#global-nav-side .gns-more-popover .gns-more-app .gns-icon {
    flex: 0 0 auto;
    box-sizing: border-box;
    width: 28px;
    height: 28px;
    padding: 5px;
    aspect-ratio: 1 / 1;
    border-radius: 6px;
}
/* Popover icons: line icons keep their stroke, filled icons their fill —
 * both painted in the muted ink, accent for the current app. */
.gns-more-popover .gns-more-app .gns-icon.app-icon * {
    fill: var(--gns-muted) !important;
}
.gns-more-popover .gns-more-app .gns-icon.rail-icon * {
    fill: none !important;
    stroke: var(--gns-muted) !important;
}
.gns-more-popover .gns-more-app.current .gns-more-link {
    color: var(--app-colour, var(--gns-accent));
    font-weight: 600;
}
/* Active app: icon tile fills with the app's colour and the glyph turns
 * white (fill for filled icons, stroke for line icons) — the same
 * white-on-app-colour treatment the rail uses for its current item. */
#global-nav-side .gns-more-popover .gns-more-app.current .gns-icon {
    background-color: var(--app-colour, var(--gns-accent));
}
#global-nav-side .gns-more-popover .gns-more-app.current .gns-icon.app-icon * {
    fill: #fff !important;
}
#global-nav-side .gns-more-popover .gns-more-app.current .gns-icon.rail-icon * {
    fill: none !important;
    stroke: #fff !important;
}
.gns-more-popover .gns-more-label {
    /* min-width: 0 lets the label shrink below its content width inside the
     * flex link, so overflow/ellipsis engages instead of pushing the row wide. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The pat-back-button leads its toolbar-section — it must sit before
 * any sibling view-option icons (e.g. the visibility toggle) regardless
 * of source order. flex order: -1 wins whether the back button is a
 * direct child of the view-options section (Library detail) or is itself
 * wrapped in its own nested view-options section (pat_ueber_back_button).
 * Applies to Side AND Compact (Cloud) — both drop the legacy top-nav
 * mobile back proxy, so in both the toolbar's own back button is the
 * mobile back affordance (re-shown in the ≤768px block below). */
body:is([data-global-nav="side"], [data-global-nav="compact"]) .pat-toolbar .toolbar-section .pat-back-button,
body:is([data-global-nav="side"], [data-global-nav="compact"]) .pat-toolbar .toolbar-section > .toolbar-section.view-options:has(> .pat-back-button) {
    order: -1;
}

/* The Home dashboard's back-to-landing button is a mobile-only affordance
 * in Side mode: on desktop the rail's Home button already returns there,
 * so hide it there. (On mobile the rule below re-shows it, restyled as a
 * toolbar icon.)
 *
 * The in-app .document-body back button is NOT hidden on desktop: the rail
 * only switches apps, it can't pop a detail view back to the list it lives
 * in (e.g. a Library document back to the Library), so that toolbar
 * pat-back-button has to stay visible at every width. */
@media only screen and (min-width: calc(768px + 1px)) {
    body[data-global-nav="side"] #portal .pat-toolbar .pat-back-button {
        display: none !important;
    }
}

/* A toolbar whose ONLY content is the reveal-the-sidebar back button
 * (button.icon.back) is hidden on non-mobile by _canvas-toolbar.scss —
 * the button has nothing to do there, leaving an empty strip. That rule
 * deliberately KEEPS the bar on mobile, where the button is the page's
 * back affordance. But in Top (non-Side) nav mode the legacy mobile
 * back-nav proxy already carries that affordance, so the bar is just an
 * empty strip on mobile too — hide it. Side and Compact modes are
 * excluded: there the proxy is dropped and this toolbar back button IS
 * the mobile back. Selector mirrors the _canvas-toolbar.scss rule
 * (flattened :not(:has()) — nested :has() is invalid). */
@media only screen and (max-width: 768px) {
    body:not([data-global-nav="side"]):not([data-global-nav="compact"]) .pat-toolbar:has(.toolbar-section.view-options > button.icon.back):not(:has(.toolbar-section.quick-functions)):not(:has(.toolbar-section.editor)):not(:has(.toolbar-section.navigation)):not(:has(.toolbar-section.view-options > :not(button.icon.back))) {
        display: none;
    }
}

/* The contacts profile/detail back toolbar (#contact-nav-toolbar) is hidden
 * on small screens by the contacts App.css (its ``--screen-small`` rule).
 * That fits Top nav — the legacy mobile nav-proxy carries "back" there and
 * the toolbar's pat-back-button is hidden anyway. In Side and Compact
 * modes the proxy is gone and that pat-back-button IS the back
 * affordance (restyled as a leading icon above), so keep the toolbar
 * visible. !important + later source beats the App.css container-query
 * rule. */
@media only screen and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #contact-nav-toolbar {
        display: block !important;
    }
}

/* ------------------------------------------------------------------ */
/* ≤768px: the rail becomes a bottom tab bar                           */
/* ------------------------------------------------------------------ */

@media only screen and (max-width: 768px) {
    /* Side and Compact (Cloud) modes drop the legacy mobile back-nav proxy
     * (quaive-mobile-navigation renders only for global_nav == 'top' — see
     * global_header.html), so the app toolbar's own .pat-back-button has
     * to carry the back affordance on small screens. _toolbar.scss hides it
     * there for the top-nav layout; re-show it while Side/Compact is active
     * and restyle it as a plain toolbar .icon (square, label hidden,
     * left-open glyph in :before). The restyle itself lives with the
     * component — see the ≤768px Side/Compact block in
     * components/pat-back-button.css. Only the toolbar-grid placement
     * rules below stay here (they are nav-mode layout, not the button). */

    /* When a .navigation section is present the mobile toolbar grid
     * squeezes its column down to a single button width (see _toolbar.scss),
     * leaving no room for the title — it shows only as a clipped sliver
     * beside the back icon. Drop it; the back icon carries the context.
     * Toolbars with no navigation section give the title the full first
     * column, so those keep their title. Mirrors the team-spaces/registry
     * small-screen "title shrinks away where there's no room" rule. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #portal .pat-toolbar:has(.toolbar-section.navigation) .toolbar-title,
    body:is([data-global-nav="side"], [data-global-nav="compact"]) .document-body .pat-toolbar:has(.toolbar-section.navigation) .toolbar-title {
        display: none;
    }

    /* The pat-back-button lives in a .view-options section, not the
     * .navigation one. On desktop that section is already the toolbar's
     * leading (first) grid column, but _toolbar.scss bumps view-options to
     * the MIDDLE column on small screens whenever an app is open with a
     * navigation section — pushing the back button off the leading edge.
     * Hand the leading slot back to the section that carries the back
     * button so it's always the first icon, like the native .icon.back.
     * (The title that normally sits first is hidden above, so the column
     * is free.) Other view-options sections keep the middle placement. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #portal .pat-toolbar .toolbar-section.view-options:has(.pat-back-button),
    body:is([data-global-nav="side"], [data-global-nav="compact"]) .document-body .pat-toolbar .toolbar-section.view-options:has(.pat-back-button) {
        grid-area: first !important;
        justify-self: start !important;
    }

    /* Floating bottom bar — same recipe as the workspace-tabs bar on
     * small screens: inset pill, translucent blur background, shadow,
     * superimposed over the content. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side {
        top: auto;
        bottom: 10px;
        left: 10px;
        right: 10px;
        width: auto;
        height: var(--workspace-tabs-bar-height, var(--gns-mobile-height));
        flex-direction: row;
        align-items: stretch;
        /* Items size to content and spread across the bar; this pins the
         * first and last icon to the two ends at a constant distance from
         * the sides, regardless of how many icons are shown. The varying
         * count is absorbed by the gaps between the inner icons. */
        justify-content: space-between;
        /* The same surface recipe the chat composer wears (its own rule in
           messages/styling reads the first of these two tokens), so the two
           islands sharing the bottom of the screen are made of one material
           and a theme tunes them together (Daniel, 2026-08-12). The blur was
           a hand-rolled blur/saturate/brightness stack that no theme could
           reach. */
        background-color: var(--pat-toolbar-button-background-colour);
        -webkit-backdrop-filter: var(--pat-surface-backdrop-filter);
        backdrop-filter: var(--pat-surface-backdrop-filter);
        box-shadow: var(--pat-toolbar-box-shadow, var(--default-box-shadow));
        border-radius: var(--border-radii, 8px);
        border-top: none;
        overflow: hidden;
        padding: 0 8px;
        /* Auto-hide on scroll: slide down out of sight (see the
         * body.gns-scrolled-away rule below) and glide back on scroll-up.
         *
         * Declared ONCE, here on the element, and deliberately not repeated in
         * the states that move it. A transition is read from the state being
         * transitioned TO, so three rules each naming their own duration meant
         * the two directions ran on different clocks and an interrupted slide
         * could land in a state whose rule said something else — which is what
         * "the second scroll jumps without transition" looks like (reported
         * 2026-08-12). One declaration cannot disagree with itself. */
        transition: transform calc(var(--base-animation-duration, 0.25s) * 2) ease;
    }
    /* Scrolling down pushes the bar fully below the viewport edge (its own
     * height + a buffer that clears the bottom inset and drop shadow);
     * scrolling up removes the class and it slides back into place. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]).gns-scrolled-away #global-nav-side {
        transform: translateY(calc(100% + 40px));
    }
    /* Collabora full-screen editing: the floating bar is superimposed over
     * the content, so on small screens it overlaps the office editor's own
     * toolbars/handles. Drop the rail entirely while a Collabora iframe is
     * open in the browser — same for the OnlyOffice (EUO) editor shell —
     * the editor (with its built-in close button)
     * gets the whole viewport. :has() is live, so the bar returns the moment
     * the editor shell leaves the DOM. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]):has(.collabora-editor-shell) #global-nav-side,
    body:is([data-global-nav="side"], [data-global-nav="compact"]):has(.onlyoffice-editor-shell) #global-nav-side {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-spaces {
        display: none;
    }
    /* The header (logo + user avatar) is dropped on mobile — Home lives
     * in the brandmark button; the personal menu stays in the apps grid. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-header {
        display: none;
    }
    /* No global header on the phone, exactly as in Side mode — where the rail
     * replaces it outright (the `display: none !important` block further up).
     * Compact's header used to be the ONLY chrome a phone had, so base.html
     * kept it: the candy box, the avatar and the app labels were the whole
     * navigation. The bottom bar carries that now, and two chrome strips for
     * one small screen is one too many (Daniel, 2026-08-11).
     *
     * Zero the height variable with it: half the compact rules position
     * against --global-header-height (the top shadow, the sticky offsets, the
     * mobile shell's document min-height), and leaving it at 54px reserves a
     * band for a header that is no longer painted. */
    body[data-global-nav="compact"] {
        --global-header-height: 0px;
    }
    body[data-global-nav="compact"] #global-header {
        display: none !important;
    }

    /* One/Pro: no More popover on the bar. The candy box replaces it — it
     * brings the portal forward and swipes the dashboard carousel to the Apps
     * portlet, so the launcher is a page of the bento rather than a popover
     * listing whichever apps happened not to fit (Daniel, 2026-08-11). Hub
     * keeps its More entry.
     *
     * !important because the per-width icon-cap blocks further down are LATER
     * media queries that set .gns-more-item's display themselves; without it
     * they would put the popover trigger back at every breakpoint above the
     * narrowest. */
    body[data-global-nav="compact"] #global-nav-side .gns-more-item,
    body[data-global-nav="compact"] #global-nav-side .gns-more-popover {
        display: none !important;
    }
    body[data-global-nav="compact"] #global-nav-side .gns-candy-item {
        display: block;
        /* Same slot the More entry held: last, at the bar's trailing end
         * (see global-nav-mobile-order.css for the order scale). */
        order: 2;
    }

    /* No user avatar on the bar either (Daniel, 2026-08-11). The personal
     * menu trigger is the rail's last child and sits OUTSIDE .gns-header, so
     * the rule above never reached it: on a 375px bar it was taking 219 of
     * the 355px — the whole row's worth of app icons — for one avatar and a
     * hub name. The personal menu is reachable from the header's own avatar
     * and from the apps grid; the bar is for apps. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-user {
        display: none;
    }
    /* Brandmark Home button is the bar's leading item (order 0), pinned to
     * the left end by the bar's space-between. Matches an app tab's box
     * width (44px icon + 2px link padding each side) so the brandmark
     * occupies the same slot as the icons; its 32px image stays centred. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-mobile-home {
        order: 0;
        position: relative;
        display: flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        width: 48px;
        min-width: 0;
        padding: 0;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-mobile-home img {
        width: 32px;
        height: 32px;
        object-fit: contain;
    }
    /* display:contents dissolves the .gns-apps box so its .gns-item rows
     * become direct flex children of the bar, spread across the row by the
     * bar's space-between alongside the home button. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps {
        display: contents;
    }
    /* Cap the app icons on the bar; the rest are reached via the "More
     * apps" popover. Three by default — the cap rises with width below,
     * so narrower screens move more icons into the popover.
     * :not(.gns-more-item):not(.gns-candy-item) keeps the trailing More entry on the bar. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(n+4) {
        display: none;
    }
    /* The More entry is the bar's trailing item, mobile only — pinned to
     * the right end by the bar's space-between. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-more-item {
        display: block;
        flex: 0 0 auto;
        min-width: 0;
    }
    /* …but drop it when nothing overflows (no app past the cap), so it
     * never opens an empty popover. The threshold rises with the cap in
     * the breakpoints below. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps:not(:has(.gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(4))) .gns-more-item {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-item {
        flex: 0 0 auto;
        min-width: 0;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-link {
        flex-direction: column;
        justify-content: center;
        gap: 2px;
        padding: 2px;
        height: 100%;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-icon {
        width: 44px;
        height: 44px;
        margin: 0;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-icon.app-icon {
        padding: 7px;
    }
    /* More-apps popover: tighten the app-icon tile padding on mobile.
     * Class-count beats the base popover rule (padding: 5px) above. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-more-popover .gns-more-app .gns-icon.app-icon {
        padding: 4px;
    }
    /* Active app: rounder tile square on the mobile bar. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-item.current .gns-icon.app-icon,
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-link[aria-current="page"] .gns-icon.app-icon {
        border-radius: var(--gns-mobile-active-tile-radius, 13px);
    }
    /* Mobile bar iconography is monochrome black — app glyphs and the
     * brandmark Home button alike. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-icon.app-icon * {
        fill: #000 !important;
    }
    /* Rail line icons: colour the stroke black, fill stays empty */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-icon.rail-icon * {
        fill: none !important;
        stroke: #000 !important;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-mobile-home img {
        filter: brightness(0);
    }
    /* Home screen current (no app on screen): the brandmark sits on an
     * accent tile, mirroring the active app icon. The tile is a ::before
     * (a background on the img would be whitened by the invert filter);
     * the brandmark itself turns white via brightness(0) invert(1). */
    body:is([data-global-nav="side"], [data-global-nav="compact"]):not(:has(#app-space.state-on)) #global-nav-side .gns-mobile-home::before {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: 44px;
        height: 44px;
        transform: translate(-50%, -50%);
        border-radius: var(--gns-mobile-active-tile-radius, 13px);
        background: var(--colour-accent, var(--gns-accent));
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]):not(:has(#app-space.state-on)) #global-nav-side .gns-mobile-home img {
        position: relative;
        z-index: 1;
        filter: brightness(0) invert(1);
    }
    /* Icons only on the bar — no tab text (like the workspace tabs) */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-label {
        display: none;
    }
    
    /* No hover tooltips on touch — labels are inline anyway */
    @media (hover: hover) {
      body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-link[data-gns-label]:hover::after {
          content: none;
      }
    }
    
    /* No hover effects on the touch bar — a tap shouldn't leave a wash */
    @media (hover: hover) {
      body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-link:hover,
      body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-mobile-home:hover {
          background: transparent;
      }
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-badge {
        position: absolute;
        top: 4px;
        right: 50%;
        margin-right: -20px;
    }
    /* The bar floats OVER the content (like the workspace tabs) — no
     * bottom offset on #app-space/#portal; content scrolls under it. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #app-space,
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #portal {
        margin-left: 0;
        margin-right: 0;
    }
    /* …but a scrollable sidebar would otherwise end UNDER the floating
     * bar, hiding its last items. Pad its scroll content by the bar's
     * full footprint (height + its bottom inset + the safe area) plus a
     * little breathing room, so everything can scroll into view. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #app-space .sidebar-content,
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #app-space #sidebar-content {
        box-sizing: border-box;
        padding-bottom: calc(
            var(--workspace-tabs-bar-height, var(--gns-mobile-height))
            + 0.5 * var(--small-screen-padding, 10px)
            + env(safe-area-inset-bottom, 0px)
            + 20px
        );
    }
    /* Same story for document bodies: pad the bottom by the bar's full
     * footprint so a page's last lines clear the floating bar instead of
     * hiding behind it. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) .document-body {
        padding-bottom: calc(
            var(--workspace-tabs-bar-height, var(--gns-mobile-height))
            + 0.5 * var(--small-screen-padding, 10px)
            + env(safe-area-inset-bottom, 0px)
            + 20px
        );
    }
    /* …except where a sticky COMPOSER already owns the bottom of the scroller
     * (Messages, Kiki). Two things about that padding go wrong there: the
     * composer is the scroller's last flow child, so nothing can hide behind
     * the bar anyway, and — measured 2026-08-12 — a sticky element's inset is
     * resolved against the scrollport's CONTENT box, so the padding is added
     * to the composer's own `bottom` and it floated 178px up instead of the 85
     * it asks for. Let the composer's inset be the only thing holding it off
     * the dock. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) .document-body:has(.kik-chat-composer) {
        padding-bottom: 0;
    }
    /* Portal / dashboard content pane: same story — pad its scroll bottom
     * by the floating bar's full footprint (height + bottom inset + safe
     * area) plus breathing room so the last portlets clear the bar. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #portal-content-pane {
        padding-bottom: calc(
            var(--workspace-tabs-bar-height, var(--gns-mobile-height))
            + 0.5 * var(--small-screen-padding, 10px)
            + env(safe-area-inset-bottom, 0px)
            + 20px
        );
    }
    /* Calendar canvas: small uniform insets, with the bottom raised clear
     * of the floating bar. (position:absolute lives in the desktop media
     * query, so re-assert it here for the insets to take effect.) */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #app-space .application-calendar .pat-calendar#calendar-app {
        position: absolute;
        top: 8px;
        right: 8px;
        bottom: 83px;
        left: 8px;
    }
    /* Meetings (Jitsi) canvas: same insets as the calendar above, so it
     * sits clear of the floating bar. */
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #app-space .application-body.application-meetings .pat-jitsi {
        position: absolute;
        top: 8px;
        right: 8px;
        bottom: 83px;
        left: 8px;
    }
    /* NB: the dashboard's bare-feed styling (single full-width column, no
     * title, no panel chrome) is NOT here — it's driven by the Size 1
     * container query (gns-dash width < 660px) above, so it tracks the
     * actual column width rather than the viewport. */
}

/* Wider phones have room for more icons on the bar before spilling into
 * the More popover: raise the cap a notch per breakpoint. Each re-shows
 * the next app (overriding the base nth-child(n+6) hide) and pushes the
 * bar's hide threshold out by one.
 *
 * The popover lists ONLY the apps that didn't fit: it's the same list in
 * the same order, so hiding the first N (= the bar cap) leaves exactly the
 * overflow. Each breakpoint hides one more, mirroring the bar. */
@media only screen and (max-width: 768px) {
    .gns-more-popover .gns-more-app:nth-child(-n+3) {
        display: none;
    }
}
/* 360px covers the narrowest mainstream phones (iPhone 13 mini / SE = 375px,
 * 360px-class Androids): the trimmed icon footprint above leaves room for a
 * 4th app on the bar at that width, where the base band fit only 3. */
@media only screen and (min-width: 360px) and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(-n+4) {
        display: block;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(n+5) {
        display: none;
    }
    .gns-more-popover .gns-more-app:nth-child(4) {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps:not(:has(.gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(5))) .gns-more-item {
        display: none;
    }
}
@media only screen and (min-width: 490px) and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(-n+5) {
        display: block;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(n+6) {
        display: none;
    }
    .gns-more-popover .gns-more-app:nth-child(5) {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps:not(:has(.gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(6))) .gns-more-item {
        display: none;
    }
}
@media only screen and (min-width: 580px) and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(-n+6) {
        display: block;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(n+7) {
        display: none;
    }
    .gns-more-popover .gns-more-app:nth-child(6) {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps:not(:has(.gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(7))) .gns-more-item {
        display: none;
    }
}
@media only screen and (min-width: 670px) and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(-n+7) {
        display: block;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(n+8) {
        display: none;
    }
    .gns-more-popover .gns-more-app:nth-child(7) {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps:not(:has(.gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(8))) .gns-more-item {
        display: none;
    }
}
@media only screen and (min-width: 760px) and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(-n+8) {
        display: block;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(n+9) {
        display: none;
    }
    .gns-more-popover .gns-more-app:nth-child(8) {
        display: none;
    }
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps:not(:has(.gns-item:not(.gns-more-item):not(.gns-candy-item):nth-child(9))) .gns-more-item {
        display: none;
    }
}
/* Preferences gets its own trailing tab on the mobile bottom bar. It's kept
 * off the desktop rail (see the desktop utility-apps block above) and is
 * normally an overflow-only entry, so on mobile we force it back onto the bar
 * — past the per-width app cap (hence !important, which beats the nth-child
 * hide bands above whatever Preferences' dynamic position is), pin it last
 * with flex order so it reads as the final option, and drop its now-duplicate
 * entry from the more-apps popover. Mobile only — the desktop rail is
 * untouched. */
@media only screen and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side .gns-apps .gns-item.gns-app-preferences {
        display: block !important;
        order: 1;
        flex: 0 0 auto;
        min-width: 0;
    }
    .gns-more-popover .gns-more-app.gns-app-preferences {
        display: none;
    }
}

/* Installed as a PWA (standalone): float the bar a little further from the
 * screen edges than in a normal browser tab (10px base above). */
@media only screen and (max-width: 768px) and (display-mode: standalone) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) #global-nav-side {
        bottom: 23px;
        left: 23px;
        right: 23px;
    }
}

@keyframes gns-sidebar-slide-in {
    from {
        transform: translateX(-48px);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* RTL twin — sidebars live on the right, so they arrive from there */
@keyframes gns-sidebar-slide-in-rtl {
    from {
        transform: translateX(48px);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* ------------------------------------------------------------------ */
/* Reduced motion                                                      */
/* ------------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
    #global-nav-side,
    #global-nav-side .gns-platform-logo img,
    #global-nav-side .gns-hub,
    #global-nav-side .gns-hub-name,
    #global-nav-side .gns-platform-name,
    #global-nav-side .gns-section-title,
    #global-nav-side .gns-label,
    #global-nav-side .gns-link,
    #global-nav-side .gns-icon.app-icon,
    body[data-global-nav="side"] #app-space,
    body[data-global-nav="side"] #portal {
        transition: none;
    }
    body[data-global-nav="side"] #app-space .application-body.sidebar-left-open aside.sidebar.left {
        animation: none;
    }
}

/* ================================================================== */
/* Compact mode (body[data-global-nav="compact"])                      */
/* ------------------------------------------------------------------ */
/* Based on Tabs mode: keeps the global header + portal, but drops the  */
/* open-app tabs bar and the portal quick-nav. A candy-box launcher in  */
/* the header (next to the personal menu) opens a pat-tooltip whose     */
/* content is the apps grid — the apps inject into the SPA shell like    */
/* the rail, so no tab/pane switching is needed.                        */

body[data-global-nav="compact"] #tabs-bar,
body[data-global-nav="compact"] .portal-quick-nav {
    display: none !important;
}

/* Kill the two legacy top-chrome decorations #app-space paints for the classic
 * header: the drop-shadow gradient (::before — forced visible by
 * body:not(:has(#portal-tabs)), and Compact mode has no #portal-tabs) and the
 * separation line above the app area (state-on::after). Compact/Cloud mode's
 * own header supplies its edge, so both read as stray artefacts here — same
 * cleanup Side mode does for the ::before shadow above. */
body[data-global-nav="compact"] #app-space:not(.injecting)::before,
body[data-global-nav="compact"] #app-space.state-on::after {
    opacity: 0 !important;
}

/* With the header shadow gone (above), paint a faint 4px shadow just under the
 * cloud header, spanning the FULL viewport width, that reads as content sliding
 * beneath it. It hangs off #main::after rather than the content panes: a
 * pane-level ::after is trapped in that pane's stacking context —
 * .document-body sits at z-index 0 beside the z-index 1 sidebar, so its shadow
 * was clipped to the content area and never reached over the sidebar. #main is
 * the app shell — a position:fixed box pinned to all four viewport edges — so
 * its ::after spans the whole viewport and, sitting a level above #app-space
 * (z-index 11) with z-index 12, clears the sidebar and every app pane
 * edge-to-edge. position:fixed (offset by the header height) holds it in place
 * as content scrolls beneath.
 *
 * Both the fill and the thickness are read from vars with the default (a faint
 * 4px gradient) supplied as the var() *fallback* rather than declared here — so
 * the only place that ever sets these vars is a theme that wants to override
 * them. The ink theme (assets/themes/ink/style/all.css) sets them at :root to a
 * crisp 1px black line; because this file loads after the brand sheet, an
 * override declared on a selector here would beat ink — the fallback keeps ink
 * (:root) the sole declaration, so it wins. */
body[data-global-nav="compact"] #main::after {
    content: "";
    position: fixed;
    top: var(--global-header-height);
    right: 0;
    left: 0;
    height: var(--kik-cloud-top-shadow-height, 4px);
    background: var(--kik-cloud-top-shadow,
        linear-gradient(
            to bottom,
            color-mix(in srgb, black 8%, transparent) 0%,
            transparent 100%));
    z-index: 12;
    pointer-events: none;
}

/* Mobile (phones ≤680px): hide the cloud-header top shadow entirely. */
@media (max-width: 680px) {
    body[data-global-nav="compact"] #main::after {
        display: none;
    }
}

/* No tabs bar in Compact mode, so pull the app sidebar flush to the top/left
 * of the app area (reclaiming the space the tabs bar used to occupy). */
body[data-global-nav="compact"] .sidebar {
    --sidebar-margin-top: 0;
    --sidebar-margin-bottom: 0;
    --sidebar-margin-left: 0;
}
/* White sidebars in Compact mode. Set the variable (not background-color
 * directly) so the sidebar toggle — which also reads
 * --sidebar-background-colour — picks up the same white.
 * Also zero the sidebar's left margin HERE (on the document-body's ancestor,
 * not just on .sidebar): the document body computes its open-sidebar left
 * offset as calc(--sidebar-width + --sidebar-margin-left) in its own scope, so
 * without this it kept the default 15px margin and sat slightly too far right.
 * Now its left equals the sidebar's effective width. */
body[data-global-nav="compact"] .application-body {
    --sidebar-background-colour: white;
    --sidebar-margin-left: 0;
    /* Override the document body's open-sidebar left offset directly: the base
     * value is calc(--sidebar-width + --sidebar-margin-left), and the inner
     * --sidebar-margin-left doesn't pick up the 0 above at the document body's
     * scope (it keeps the 15px default), so the document body sat 15px too far
     * right. Drop the margin term entirely — left == the sidebar width. */
    --document-body-left-distance-sidebar-open: var(--sidebar-width);
}
/* Events app: white application-body background in Compact mode. */
body[data-global-nav="compact"] .application-body.application-events {
    --application-body-background-colour: white;
}
body[data-global-nav="compact"] aside.sidebar.left {
    height: 100%;
    border-radius: 0;
}
body[data-global-nav="compact"] aside.sidebar.left .sidebar-content {
    border-radius: 0;
}
/* A large sidebar (e.g. the Tasks list) spans the full viewport width in
 * Compact mode. Its base width is calc(100% + …) of the app body, which is
 * narrower than the viewport and left a strip on the right; pin it to the
 * viewport instead. This also pushes the document body fully off-screen, since
 * its open-sidebar left offset tracks --sidebar-width. */
body[data-global-nav="compact"] .application-body.sidebar-large aside.sidebar.left {
    --sidebar-width: 100vw;
}
/* The 100vw above only retargets --sidebar-width, but the OPEN large sidebar's
 * actual `width` is set by a separate, higher-specificity base rule:
 *   @container main (min-width: 769px) {
 *     .sidebar-left-open.sidebar-large aside.sidebar.left {
 *       width: calc(100% - 2 * var(--sidebar-margin-left));
 *     }
 *   }
 * The 2 × --sidebar-margin-left inset is the right thing in Tabs/Rail mode, but
 * in Compact it stops the sidebar short and leaves a strip on the right. The
 * sidebar is absolutely positioned and its containing block is #app-space
 * (left: 0; right: 0 — always the full viewport), so 100% with no inset spans
 * the viewport exactly (and, unlike 100vw, never adds a scrollbar-gutter
 * overflow). The extra body[data-global-nav]/.application-body terms out-rank
 * the base selector. */
body[data-global-nav="compact"] .application-body.sidebar-left-open.sidebar-large aside.sidebar.left {
    width: 100%;
}
/* Pin the document body beside the sidebar from the narrow-desktop breakpoint
 * (769px) in Compact mode. The base only shifted it right at >=1280px; between
 * 769-1280px it fell back to left:0 (full width, sidebar overlapping it) —
 * which read as "document body too wide". Compact has minimal chrome, so the
 * side-by-side layout should kick in earlier.
 *
 * Asks the CONTAINER, not the viewport: as a @media rule this measured the
 * screen, so a 400px-wide window on a 1400px desktop still got the full
 * desktop sidebar offset — the sidebar inside it was in phone mode, and the
 * content was shoved sideways by a gutter nothing occupied. (The base rule in
 * document-body.css has meanwhile moved from 1280px to this same >768px
 * container gate, so this block now only adds the Compact specificity.)
 *
 * Carries the same ``:has(aside.sidebar)`` gate as that base rule: the blanket
 * ``sidebar-left-open`` on every app body declares a sidebar's landing state,
 * not its existence, so only a body that HOLDS one reserves the gutter. */
@container main (width > 768px) {
    body[data-global-nav="compact"] .sidebar-left-open:not(.sidebar-left-closed):has(aside.sidebar) .document-body,
    body[data-global-nav="compact"] .sidebar-left-open:not(.sidebar-left-closed):has(aside.sidebar) #document-body {
        left: var(--document-body-left-distance-sidebar-open);
    }
    body[dir="rtl"][data-global-nav="compact"] .sidebar-left-open:not(.sidebar-left-closed):has(aside.sidebar) .document-body,
    body[dir="rtl"][data-global-nav="compact"] .sidebar-left-open:not(.sidebar-left-closed):has(aside.sidebar) #document-body {
        left: var(--document-body-right-distance);
        right: var(--document-body-left-distance-sidebar-open);
    }
}

/* Messages (Element) app fills its app body in Compact mode — pin all four
 * insets to 0 (the base rule offsets by the --sidebar-margin-* vars). */
body[data-global-nav="compact"] .application-body.application-messages .pat-element {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}
body[data-global-nav="compact"] .pat-element iframe {
    border-radius: 0;
}
/* Collabora office editor fills its app body flush in Compact mode. */
body[data-global-nav="compact"] .document-content .collabora-editor-frame {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border-radius: 0;
    box-shadow: none;
}

/* Compact-mode app labels (multi-tasking): one label per OPEN app, laid out
 * left-to-right in #kik-app-labels. Each label is an app-colour-bordered pill of
 * [icon · name · close]. Show/hide + active highlighting are driven entirely by
 * the per-app :has() rules in the kik_app_label partial's inline <style> (no
 * JS); here we set the row layout, the box and the appearance.
 *
 * #kik-app-labels fills the header between the home brand and the right-floated
 * controls (avatar / candy box / favourites). It's the sole in-flow child of
 * #global-header and establishes a flex formatting context, so it shrinks to
 * clear those right floats automatically — no hard-coded width reserve. Its
 * margin-left clears the absolutely-positioned home tile (15px inset + 36px tile
 * + gap). overflow:hidden clips any labels that still don't fit once shrunk. */
/* #kik-app-labels is a BLOCK box (overflow:hidden establishes its own formatting
 * context) so its auto width FILLS the gap left of the right-floated controls in
 * every engine. The flex row lives on the inner .kik-app-labels-row. Flexing
 * #kik-app-labels directly breaks WebKit: a flex formatting context beside a
 * float is sized shrink-to-fit, collapsing to min-content — and with the names'
 * min-width:0 + ellipsis that is just the icons, so on iPad every name ellipsised
 * to nothing even on an empty header. A block box fills, giving the inner flex a
 * real width so names only ellipsise when genuinely crowded. */
#global-header .kik-app-labels {
    display: block;
    height: var(--global-header-height);
    /* No left margin: the absolutely-positioned home tile it used to clear is
     * gone (2026-08-16), so the row starts at the header's own padding. */
    overflow: hidden;
    /* The row fills the full free width, but its empty stretch must not eat
     * taps meant for the content scrolled under the (transparent, pointer-events
     * :none) header on small screens — so only the labels themselves are live. */
    pointer-events: none;
}
#global-header .kik-app-labels .kik-app-labels-row {
    display: flex;
    align-items: center;
    gap: 6px;
    height: 100%;
    overflow: hidden;
}
#global-header .kik-app-labels kik-app-label {
    pointer-events: auto;
}
body[dir="rtl"] #global-header .kik-app-labels {
    margin-left: 0;
    margin-right: 44px;
}
kik-app-label {
    position: relative;
    align-items: center;
    gap: 0;
    /* Idle: icon glyph + name in the muted base header ink (#6B6560, the candy
     * box's colour), and a transparent border. The per-app active rule
     * (kik_app_label.html) overrides --kal-text / --kal-border to the app colour. */
    color: var(--kal-text, #6B6560);
    /* Same overall height as the global-header icons (36px), the 1.5px border
     * included (box-sizing). The inner .kal-icon is sized down to fit inside. */
    box-sizing: border-box;
    height: 36px;
    padding: 0;
    border: 1.5px solid var(--kal-border, transparent);
    /* The header's corner. Shared, because everything that rides this bar has
       to round the same way — the media-player chip's album art takes it too
       (base.css). Change it here and the row follows. */
    border-radius: var(--kal-corner-radius, 8px);
    transition: border-color 0.2s;
    /* Flex item: shrink when the row is crowded (min-width:0 lets the name
     * ellipsis), while the icon + close inside it never shrink. */
    flex: 0 1 auto;
    min-width: 0;
}

/* Tab hover keeps only the background wash and the border line (Daniel,
 * 2026-08-15) — the icon/name darkening is retired. */
/* The icon tile. Idle: transparent background, glyph in the app colour (the
 * label's border already carries the app identity). Active: the per-app rule
 * fills the tile with the app colour, flips the glyph white and scales it to 0.8
 * so a ring of the label's (white) background shows around it — all via the
 * --kal-icon-* custom properties, with a transition on the transform. */
kik-app-label .kal-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    /* Square tile, DEFINITE 33px (the label's 36px height minus its 1.5px
     * border top+bottom). The app-icon SVGs are viewBox-only (no intrinsic px
     * size). A definite width matters in TWO WebKit places:
     *   1. Visual: width:auto + the flex default min-width:auto lets the
     *      indeterminate SVG width balloon the tile to full width (old iOS bug).
     *   2. Layout: WebKit resolves an auto/aspect-ratio flex item's FLEX BASIS
     *      from the SVG's ~300px fallback intrinsic, NOT the aspect-ratio-derived
     *      width, so each tab is measured ~300px wide → the row over-subscribes →
     *      flex-shrink ellipsises every .kal-name even with a near-empty header
     *      (tiny icons + acres of space + truncated names, Safari/iPad only).
     * A definite width fixes both: real 33px basis, names ellipsise only when the
     * row is genuinely crowded. (Mirrored inline in kik_app_label.html so it ships
     * without a SASS recompile — the assets submodule is volatile.) */
    width: 33px;
    min-width: 33px;
    height: 33px;
    flex: 0 0 33px;
    /* contain:size makes WebKit measure the tile purely from its CSS size and
     * IGNORE the viewBox-only SVG's phantom intrinsic — the definite width above
     * is meant to do this alone, but some WebKit builds still leak a flex item's
     * content intrinsic into the ancestor (tab row) sizing pass, re-inflating the
     * row and re-ellipsising the names. contain:size + overflow:hidden slams that
     * door shut. */
    contain: size;
    overflow: hidden;
    padding: 4px;
    border-radius: 6px;
    background-color: var(--kal-icon-bg, transparent);
    color: var(--kal-icon-fg, #6B6560);
    transform: scale(var(--kal-icon-scale, 1));
    transition: all 0.2s;
    /* The icon is the app-label menu trigger; disable it on inactive tabs so a
     * click there can't open the menu. The active tab re-enables it via
     * --kal-icon-pointer (set in the per-app active rule in kik_app_label.html). */
    pointer-events: var(--kal-icon-pointer, none);
}
/* Roughly half the app icons are authored as solid white silhouettes
 * (fill="white"); tie their fill to currentColor so they follow .kal-icon's
 * colour exactly like the outline (stroke: currentColor) icons do. Outline icons
 * (fill: none) are intentionally NOT matched, so they stay outlines. */
kik-app-label .kal-icon svg[fill="white"] *,
kik-app-label .kal-icon svg [fill="white"] {
    /* Ink flips this to a transparent fill + a NON-scaling stroke so the
     * solid-silhouette icons read as black outlines (like the app grid) at the
     * tiny tab size; without non-scaling-stroke a 1px stroke in the 512 viewBox
     * collapses to sub-pixel and the glyph vanishes. Defaults (fill:currentColor,
     * no stroke) keep every other brand's filled rendering. Target the shapes,
     * not the <svg>, so vector-effect (not inherited) actually applies. */
    fill: var(--kal-icon-fill, currentColor);
    stroke: var(--kal-icon-stroke, none);
    stroke-width: var(--kal-icon-stroke-width, 0);
    vector-effect: non-scaling-stroke;
}
kik-app-label .kal-icon svg {
    width: 100%;
    height: 100%;
    transform: scale(var(--kal-icon-svg-scale, 0.8));
}
kik-app-label .kal-icon svg * {
    transition: all 0.2s;
}
/* The tab body: a pat-switch <button> that activates this app, reset to read as
 * plain text (it inherits the label's app colour). */
kik-app-label .kal-name {
    appearance: none;
    border: 0;
    background: transparent;
    padding: 0;
    margin: 0;
    color: inherit;
    font-family: inherit;
    text-align: left;
    cursor: default;
    font-weight: 300;
    font-size: 22px;
    white-space: nowrap;
    /* Only the text is sacrificed when the row is crowded (req: close + icon
     * stay intact). */
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    flex: 0 1 auto;
}
/* Stretched-link hit-layer: the name button's switch covers the WHOLE tab, so a
 * click anywhere on it activates the app. It sits BEHIND the icon and × (z-index
 * 1 vs their 2): an inactive icon (pointer-events:none) lets the click fall
 * through to it, while the active icon (pointer-events:auto) and the × stay on
 * top with their own actions. overflow:hidden on the button doesn't clip it —
 * the ::before's containing block is the (position:relative) kik-app-label. */
kik-app-label .kal-name::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}
kik-app-label .kal-icon,
kik-app-label .kal-close {
    position: relative;
    z-index: 2;
}
/* Close button (an <a> → close_app, so the close is recorded in the session):
 * the mark in the muted base header ink for inactive tabs (the active tab's
 * rule flips --kal-close-colour to the app colour). No background in any
 * state: the mark is --glyph-cancel-circle (kik_app_label.html), which draws
 * its OWN circle at the button's full size, so a plate behind it was a second
 * circle 1.6px outside the first. Hover is carried by the colour alone. */
kik-app-label .kal-close {
    /* The ring. It lives on the BUTTON, not on the ::before that draws the ×
       — the pseudo is a 14px box centred in this 20px one, so a border there
       drew a second, smaller circle inside this one. Colour follows the active
       state through --kal-close-border (the app colour on the active tab, 50%
       black off it), the same way --kal-close-glyph-bg drives the fill. */
    border: 1px solid var(--kal-close-border, rgba(0, 0, 0, 0.5));
    background: transparent;
    text-decoration: none;
    cursor: default;
    font-size: 14px;
    line-height: 1;
    /* No optical padding here. This described the text glyph U+00D7, which the
     * mark no longer is — kik_app_label.html collapses the anchor's own text
     * and draws --glyph-cancel on ::before, correcting the glyph there. Two
     * corrections stacked would fight; the id-specificity rule in the template
     * already zeroed this one, so it only survived as a misleading comment. */
    padding: 0;
    border-radius: 50%;
    color: var(--kal-close-colour, #6B6560);
    /* Hidden + collapsed on inactive tabs (no × taking space); the active tab's
     * rule sets --kal-close-w / --kal-close-mx / --kal-close-opacity to reveal it.
     *
     * Collapse by WIDTH, not `zoom`. `zoom` is a WebKit layout property, and zoom
     * on a flex item corrupts the tab row's intrinsic sizing: WebKit reserved each
     * inactive ×'s footprint at its UNZOOMED ~18px+margins (not the zoomed-away
     * size it painted), so every inactive tab carried a phantom close that
     * over-subscribed the row → flex-shrink ellipsised the names even with room to
     * spare (Safari/iPad only — the active tab's honest zoom:1 × never triggered
     * it). Width/margin are sized honestly by every engine: 0 when collapsed, 18px
     * on the active tab, eased. Mirrored inline in kik_app_label.html (which also
     * neutralises the stale built-CSS zoom until the submodule recompiles). */
    opacity: var(--kal-close-opacity, 0);
    transition: opacity 0.2s ease, width 0.2s ease, margin 0.2s ease;
    display: flex;
    flex: 0 0 auto;
    box-sizing: border-box;
    width: var(--kal-close-w, 0);
    min-width: var(--kal-close-w, 0);
    /* 20px, per design. NOTE the tab is 36px with a 1.5px border, so the box
     * the disc centres in is 33px — ODD — and an even disc leaves 6.5px above
     * and below, which the browser lands on a half pixel. (19px divided the 33
     * exactly, 7 and 7; 21px would too, at 6 and 6.) Width follows from
     * --kal-close-w in kik_app_label.html, which carries the same 20 so the
     * disc stays a circle. */
    height: var(--kal-close-h, 20px);
    /* visible, so the ::after hit area below can extend past this box.
     * The collapsed × no longer needs clipping: opacity 0 keeps it unseen and
     * pointer-events: none keeps it unclickable. */
    overflow: visible;
    pointer-events: none;
    align-items: center;
    justify-content: center;
    margin: 0 var(--kal-close-mx, 0);
}
/* Thumb-sized hit area without a bigger ×: an invisible pseudo extends the
 * clickable surface past the 18px visual button — square, and the full height
 * of the tab (36px). Hit-testing only, nothing painted; a click on a pseudo is
 * dispatched to its originating element, so the <a> stays the event target and
 * the pat-switch/pat-inject close wiring is untouched.
 *
 * Sized from --kal-close-hit rather than fixed, so it is literally 0×0 on
 * inactive tabs. That matters now that the button is overflow: visible — a
 * fixed 36px box would sit over every collapsed × as an invisible close target
 * and swallow clicks meant for the tab's own switch surface. The active tab's
 * rule sets it alongside --kal-close-w (see kik_app_label.html).
 *
 * (This replaced a ::before with negative insets. That never worked: the
 * button was overflow: hidden, which clips a pseudo out of the hit test as
 * surely as it clips it out of the paint, so the enlarged area it described
 * did not exist.) */
kik-app-label .kal-close::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: var(--kal-close-hit, 0);
    height: var(--kal-close-hit, 0);
    /* Re-enable hits inside the pseudo only; the button itself is
     * pointer-events: none so the × glyph, which overflows a 0-width box when
     * collapsed, cannot be clicked on an inactive tab. */
    pointer-events: auto;
}

/* No candy-box trigger rules here any more. In compact mode the launcher is a
 * <kik-app-label class="kal-candy"> holding a .kal-icon anchor — the same
 * markup as a pinned app — so the tab rules above give it its box, colour and
 * hover. (The side rail's own launcher is .cnr-candy, in cloud-nav-rail.css.) */

/* The candy-box grid is styled ONLY by .kik-apps-grid — see
 * static/libraries/kikaron/apps-grid/apps-grid.css. It used to override the
 * column count, gap, padding, label size, icon colour, stroke width and
 * border-radius here (and its colour inversion from the brand sheet), so the
 * launcher and the dashboard drifted apart and a change to 'the apps grid'
 * had to be made twice. The popover's own width lives in
 * libraries/patterns/context-menu/pat-context-menu.css, which is the shell
 * now — the .tippy-box rule that used to sit here was dead. */

/* Cloud/compact mode: the global search FIELD (base.html) is swapped for a
 * launcher button for the Search app, sitting in the field's old header slot
 * (floated right, next to the candy-box). The Search rail icon is a stroked
 * (outline) magnifier painted with currentColor, so the launcher only sets
 * `color`: the app colour by default (visible on the light header). It's pure
 * pat-switch — clicking just brings the always-present #application-body-search
 * pane forward (its pat-lazy-load anchor autoloads the search home the first
 * time it's revealed). */
#global-header .kik-search-launcher {
    float: right;
    /* Match the kik-app-label height (36px, border included), so size the
     * launcher the same and centre it vertically in the header. */
    margin-top: calc((var(--global-header-height) - 36px) / 2);
    margin-left: var(--application-label-white-space);
    box-sizing: border-box;
    width: 36px;
    height: 36px;
    padding: 5px;
    border: 0;
    border-radius: 6px;
    background-color: transparent;
    color: var(--app-colour);
    cursor: pointer;
    transition: background-color var(--base-animation-duration),
                color var(--base-animation-duration);
}
body[dir="rtl"] #global-header .kik-search-launcher {
    float: left;
    margin-left: 0;
    margin-right: var(--application-label-white-space);
}
#global-header .kik-search-launcher svg {
    display: block;
    width: 100%;
    height: 100%;
    stroke: currentColor;
}
@media (hover: hover) {
  #global-header .kik-search-launcher:hover {
      background-color: rgba(0, 0, 0, 0.06);
  }
}

/* "current": Search is the active, on-screen app — fill the tile with the app
 * colour and flip the outline glyph to white so it reads against it, mirroring
 * the side-rail .gns-item.current treatment. The :has() reuses kik-app-label's
 * active-app detection (#app-space.state-on.active-app-search). This file is
 * cache-busted (asset_version), so the :has() rule can live here rather than
 * inline in the template. */
body[data-global-nav="compact"]:has(#app-space.state-on.active-app-search) #global-header .kik-search-launcher {
    background-color: var(--app-colour);
    color: #fff;
}
@media (hover: hover) {
  body[data-global-nav="compact"]:has(#app-space.state-on.active-app-search) #global-header .kik-search-launcher:hover {
      background-color: var(--app-colour);
  }
}

/* ------------------------------------------------------------------ */
/* Phones: HUB uses its own sidebar insets; One/Pro fill the screen        */
/* ------------------------------------------------------------------ */
/*
 * The mobile bar, the panes and the portal are shared by both faces (the
 * ≤768px section above matches "compact" alongside "side"), so the app shell
 * measures the same in either mode — with ONE exception, here.
 *
 * Compact zeroes the sidebar insets: a DESKTOP decision that reclaims the
 * space the tabs bar used to occupy. Hub's bar FLOATS 10px off the screen
 * edge, so a sidebar running the full height would pass behind it — hub keeps
 * its 15px inset.
 *
 * One and Pro do not: their dock is flush to the bottom edge now, and the
 * sidebar's own content already reserves the bar's footprint as padding (the
 * .sidebar-content rule above). An inset there only left a strip of backdrop
 * under the list — Daniel, 2026-08-12: the sidebar's height IS the viewport.
 *
 * Last in the file on purpose: the rules it overrides carry the same
 * specificity, so source order is what decides.
 */
@media only screen and (max-width: 768px) {
    body[data-global-nav="side"] .sidebar,
    body[data-global-nav="side"] .application-body {
        --sidebar-margin-top: var(--sidebar-margin);
        --sidebar-margin-bottom: var(--sidebar-margin);
        --sidebar-margin-left: var(--sidebar-margin);
    }
}

/* ------------------------------------------------------------------ */
/* One/Pro phones: the bar carries the candy box, and nothing else      */
/* ------------------------------------------------------------------ */
/*
 * Daniel, 2026-08-12. The apps carousel is the app switcher on these faces —
 * swipe between open apps, launcher page on the left — so a row of app icons
 * in the bar was a second switcher for the same job, and the brandmark Home
 * and the pinned Preferences were third and fourth doors to what the launcher
 * already shows. One control remains: the candy box, which jumps to the
 * launcher page.
 *
 * Hub keeps its full bar — there the icons ARE the navigation.
 *
 * Last in the file, and !important, because the per-width icon-cap blocks and
 * the pinned-Preferences rule are later media queries that set display
 * themselves (Preferences with !important of its own).
 */
@media only screen and (max-width: 768px) {
    body[data-global-nav="compact"] #global-nav-side .gns-mobile-home,
    body[data-global-nav="compact"] #global-nav-side .gns-apps .gns-item:not(.gns-candy-item) {
        display: none !important;
    }

    /* The bar spreads its children edge to edge (justify-content:
     * space-between), which parks a lone control against the left rim. The
     * candy box goes to the RIGHT rim instead (Daniel, 2026-08-12) — the
     * middle of the bar is taken: the apps carousel's rail of balls docks
     * there, centred, so this face reads left-to-right as "where you are"
     * (the balls) then "everywhere you could go" (the launcher).
     *
     * The rail seats itself absolutely against the bar's midpoint rather than
     * riding this flex line (phone_app_carousel.html, .kik-pac-dots--docked),
     * so it stays dead centre no matter how wide the candy box is — the bar's
     * own `position: fixed` (the base #global-nav-side rule) is the
     * positioning context that seating hangs off. */
    body[data-global-nav="compact"] #global-nav-side {
        justify-content: flex-end;
    }
}

/* ------------------------------------------------------------------ */
/* The dock strip: one clearance, and it yields to a focused composer   */
/* ------------------------------------------------------------------ */
/*
 * The bar is the DOCK on a phone. Two things hang off that (Daniel,
 * 2026-08-12), and both are CSS only — nothing measures a composer or a
 * keyboard in JavaScript any anymore:
 *
 *   --kik-dock-clearance is how much room the dock takes at the bottom of the
 *   screen: its 64px height, its 10px offset, and 8px of air. Anything that
 *   would otherwise sit in that strip reads this and rests above it — the
 *   carousel's dot rail, and the sticky chat composer (messages/styling).
 *
 *   While a composer HAS FOCUS the dock steps down out of the way, because
 *   that is the moment the strip is worth more to the person typing (and on a
 *   phone it is also when the keyboard has taken everything below). The travel
 *   is the scroll auto-hide's, so the two never fight. :has() on <body>: the
 *   composer is inside an app page and the bar is a sibling of #app-space.
 */
@media only screen and (max-width: 768px) {
    body:is([data-global-nav="side"], [data-global-nav="compact"]) {
        /* What anything resting ON TOP of the dock has to clear: the bar's
           10px offset from the screen edge, its 64px height, the 1px hairline
           it draws, and 10px of air — Daniel wants the chat composer exactly
           ten pixels above the dock (2026-08-12). */
        --kik-dock-clearance: calc(85px + env(safe-area-inset-bottom, 0px));
        /* The dock's icon box, stated once so the two things wearing it — the
           candy box at the right rim and the active-app icon at the left
           (phone_app_carousel.html, .kpa-icon) — cannot drift apart
           (Daniel, 2026-08-12). */
        --kik-dock-icon-size: 46px;
        --kik-dock-icon-radius: 10px;
    }

    /* The media-player chip rides IN the dock, immediately left of the candy
       box (Daniel, 2026-08-12). Its default perch is the top-left corner —
       which belonged to the icon rail, and on these faces there is no longer
       any chrome up there at all, so it floated over the app's own toolbar.

       media-player.js parents it INTO the bar on these faces (dockChip), the
       way the rail and the active-app label are; it stays outside #app-space,
       so no pat-inject can swap it out mid playback. It keeps position: fixed
       — and the bar's backdrop-filter makes the bar its containing block, so
       these offsets read against the BAR: 8px of bar padding + the candy
       box's 50px hit area + 8px of air from the right rim, and a vertical
       centre in the 64px control band above the safe area.

       Nothing here has to hide it with the dock, either: it is inside the bar
       now, so it slides away with it. */
    body[data-global-nav="compact"] .kik-media-player-chip {
        top: auto;
        left: auto;
        right: 66px;
        /* The dock's icon box, like the candy box and the active app's icon:
           one size and one radius for everything seated in this strip
           (Daniel, 2026-08-12). */
        width: var(--kik-dock-icon-size);
        height: var(--kik-dock-icon-size);
        border-radius: var(--kik-dock-icon-radius);
        bottom: calc(
            (var(--workspace-tabs-bar-height, var(--gns-mobile-height))
             - var(--kik-dock-icon-size)) / 2
            + env(safe-area-inset-bottom, 0px));
    }

    /* The candy box's own button paints WHITE — it is a .gns-link, and those
       inherit a button surface (measured rgb(255,255,255) behind a bar that is
       itself a translucent white pill, so it read as a solid chip around the
       glyph). The bar IS the surface here; the trigger is just a hit area
       (Daniel, 2026-08-12). */
    body[data-global-nav="compact"] #global-nav-side .gns-candy-trigger {
        background: transparent;
    }

    /* The candy box takes that box. The bar's generic .gns-icon is 44px, a
       size inherited from the days when the bar was a row of app tiles; the
       one control left on this face does not need to be the biggest thing in
       it. The 48px .gns-link around it is untouched, so the TAP target keeps
       its size. */
    body[data-global-nav="compact"] #global-nav-side .gns-candy-item .gns-icon {
        width: var(--kik-dock-icon-size);
        height: var(--kik-dock-icon-size);
        border-radius: var(--kik-dock-icon-radius);
    }

    body:is([data-global-nav="side"], [data-global-nav="compact"]):has(.kik-chat-composer:focus-within) #global-nav-side {
        transform: translateY(calc(100% + 40px));
    }
}

/* ------------------------------------------------------------------ */
/* A composer follows the dock down                                     */
/* ------------------------------------------------------------------ */
/*
 * The bar hides itself on a downward scroll (.gns-scrolled-away above), and a
 * composer left floating 85px up over the space it vacated reads as a bar with
 * a hole beneath it. Drop it to the plain inset — the same place the focused
 * composer sits, because it is the same situation from the other end: nothing
 * is in the dock strip.
 *
 * Here rather than in the messages stylesheet because the registry build
 * prefixes every selector in that sheet with `.application-body
 * .application-messages`, which both mangles a `body.…` selector and gives the
 * resting rule a specificity of (0,3,0) — hence the :is() here, which buys the
 * one class needed to out-rank it.
 */
@media only screen and (max-width: 768px) {
    body.gns-scrolled-away :is(.application-body, .kiki-dock) .kik-chat-composer {
        --kik-composer-inset: 15px;
    }
}

/* ------------------------------------------------------------------ */
/* One and Pro: the dock is a bar, not an island                       */
/* ------------------------------------------------------------------ */
/*
 * On the compact faces the dock runs edge to edge and sits ON the bottom of
 * the screen, square-cornered, with the home-indicator safe area added BELOW
 * its 64px of controls rather than eaten out of them (Daniel, 2026-08-12).
 * Hub keeps the floating pill.
 *
 * `compact` IS One and Pro — the two share that chrome value, which is why one
 * attribute covers both.
 *
 * Last in the file because it overrides the ≤768px bar block above AND the
 * standalone-PWA insets that follow it — all three carry the same specificity,
 * so source order is what decides.
 */
@media only screen and (max-width: 768px) {
    body[data-global-nav="compact"] {
        /* No 10px gap under the bar any more, so what rests on top of the
           dock clears its height, its hairline, the safe area it now covers,
           and the same 10px of air. */
        --kik-dock-clearance: calc(75px + env(safe-area-inset-bottom, 0px));
    }

    body[data-global-nav="compact"] #global-nav-side {
        left: 0;
        right: 0;
        bottom: 0;
        border-radius: 0;
        /* box-sizing is border-box here, so the height has to grow by the
           safe area for the padding to add space UNDER the controls instead
           of squeezing them. */
        height: calc(var(--workspace-tabs-bar-height, var(--gns-mobile-height))
                     + env(safe-area-inset-bottom, 0px));
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
}
