/* ── Stacked views (R1.3) ────────────────────────────────────────────────── */
/* Three classes, set by js/phone-view.js, and the split is the whole design:   */
/*   .is-stacked  the tiles leave the grid and flow — SHARED by both sizes      */
/*   .is-phone    + one column, compact chrome, the thumb dock   (≤620px)       */
/*   .is-tablet   + two columns, the ordinary topbar             (≤1120px)      */
/* Presentation only — the grid engine's state is never touched, so a mistake   */
/* in this file can look wrong but cannot damage a saved layout.                */
/*                                                                             */
/* The tablet band exists because it measured WORSE than the phone: at 768x1024 */
/* against a real dashboard, 30 elements were clipped by an ancestor with       */
/* `overflow: hidden`, against 9 at 390px. Nothing adapted between the phone    */
/* threshold and the desktop one, so a 32px grid column just squeezed every     */
/* widget until its contents were cut.                                          */

/* Safe areas: a phone has a notch at the top and a home indicator at the
   bottom, and the dashboard was designed for a screen with neither. */
/* `dvh`, not `100%` and not `100vh`. On a phone the browser's own toolbars are
   part of the viewport story: `100vh` is the LARGE viewport (the height the page
   would have with every toolbar retracted) and a percentage height resolves
   against an initial containing block the engines size differently. Either way
   the shell can end up taller than what is on screen, and the shell's last row
   is the dock — so the bar of controls sits just below the fold, on a page that
   does not scroll at the body level, with no way to reach it. `dvh` is the
   height that is actually visible. It does not thrash here: this view scrolls
   inside the page rather than at the root, so the toolbars do not retract. */
.is-stacked,
.is-stacked body {
  height: 100dvh;
  overflow: hidden;
}
.is-stacked body { padding-top: env(safe-area-inset-top); }
/* breakpoints.css turns the shell into a document-tall column below 1120px
   (`height: auto; min-height: 100vh`) and lets the BODY scroll. That predates
   the pager and is wrong here in a way that is easy to miss: the dock is the
   shell's last grid row, so an auto-height shell puts it 230px below the fold
   (measured on a 393x760 viewport) where no thumb will ever find it. The shell
   is a full-screen frame again; scrolling happens inside the page, which is
   what the pager expects. */
.is-stacked .shell {
  height: 100%;
  min-height: 0;
  padding: 8px 10px 0;
  gap: 8px;
}
.is-tablet .shell { padding: 10px 14px 0; gap: 10px; }

/* ── The tiles stack ─────────────────────────────────────────────────────── */
/* GridStack positions items absolutely from `gs-x`/`gs-y` attribute rules. We
   neutralise that and let normal flow take over; js/phone-view.js has already
   put the nodes in reading order. `display` is deliberately NOT touched: a tile
   hidden because it is the inactive member of a tab group carries
   data-dashboard-hidden="true" → display:none, and it must stay hidden. */
/* CSS Grid rather than a flex column, so the same rule serves one column and
   two. `align-items: start` keeps every tile at its own height instead of
   stretching the short one in a pair to match its neighbour — a 4-row clock
   blown up to the height of a 16-row media tile is empty space, not balance. */
.is-stacked .dashboard.grid-stack {
  display: grid !important;
  grid-template-columns: repeat(var(--ph-cols, 1), minmax(0, 1fr));
  align-items: start;
  gap: 10px;
  /* fitGridHeights() writes an inline pixel height sized for the desktop
     layout; stacked content is taller than that and would be clipped. */
  height: auto !important;
  min-height: 0;
  padding-bottom: 12px;
}
.is-tablet .dashboard.grid-stack { --ph-cols: 2; gap: 12px; }
.is-stacked .dashboard.grid-stack > .grid-stack-item {
  position: static !important;
  inset: auto !important;
  width: 100% !important;
  max-width: 100% !important;
  /* A DEFINITE height, not `height:auto` + `min-height`. This is the whole
     difference between the tiles working and half of them looking empty, and it
     is not obvious: a percentage height does not resolve against a parent whose
     `height` is `auto`, even when `min-height` has stretched it. Every widget
     inside a tile is built on the chain
     `.grid-stack-item-content > .panel { height: 100% }`, so with an auto-height
     item that chain collapses to the content's natural size — measured: the
     media tile rendered its title and nothing else, no artwork and no transport,
     inside a 208px box. A definite height makes each tile the same kind of box
     the desktop grid gives it, so every widget's internal layout works here
     unchanged rather than needing a phone variant of its own.
     34px/row keeps a tile close to its desktop proportions (the grid's own
     cellHeight is 35), so the user's sizing still means what it meant. */
  /* The upper bound is what a tile may take of the SCROLLING BAND, not of the
     viewport — those differ by the chrome above and below it (topbar ~51px,
     thumb dock ~57px, plus the shell's own gaps). 80vh alone is right in
     portrait and wrong on a phone lying on its side: at 430px of viewport it
     resolved to 344px against a 298px band, so no widget ever fit on screen
     whole, which reads as the clipping this view exists to remove. Measured on
     an iPhone-sized landscape viewport. */
  /* `dvh` for the same reason the shell uses it: this bound is the SCROLLING
     BAND, and a band measured against a viewport height the phone is not
     actually showing lets a tile grow past the bottom of the screen. */
  /* The lower bound is a VARIABLE, so a widget whose content has a real floor can
     raise its own — see `--ph-min` below. 180px is the general case. */
  height: clamp(var(--ph-min, 180px), calc(var(--ph-rows, 8) * 34px), min(80dvh, calc(100dvh - var(--ph-chrome, 158px)))) !important;
  min-height: 0 !important;
  transform: none !important;
}
/* ── A tile whose content cannot be compressed ───────────────────────────── */
/* 34px per grid row is a proportion, not a promise, and a widget built out of a
   FIXED number of rows has a height below which it stops being readable rather
   than merely getting tighter. The month grid is the clearest case: it is six
   week rows however short the tile is, so at the 272px an 8-row agenda tile gets
   here, each row was computed to 7.4px against 13.5px digits — every number
   painted across the two below it, which is what the calendar looked like on a
   phone (reported, and visible in the demo). The rows below give the grid a
   minimum row height, and this floor is what keeps the tile from having to
   scroll internally to honour it.
   Add to this list only when a widget's own minimum is structural like this
   one's; the answer for a widget that is merely tight is its internal scroll. */
.is-stacked .dashboard.grid-stack > .grid-stack-item:has(.agenda-panel),
.is-stacked .dashboard.grid-stack > .grid-stack-item:has(.calendar-panel) { --ph-min: 380px; }
/* A tablet has no thumb dock and a roomier topbar, so the band a tile may fill
   is taller — the number is the chrome above and below it, not a guess. */
.is-tablet { --ph-chrome: 108px; }
.is-stacked .grid-stack-item > .grid-stack-item-content {
  position: static !important;
  inset: auto !important;
  height: 100% !important;
  min-height: 0 !important;
}
/* An expanded widget owns the whole screen and does its own positioning. */
.is-stacked .grid-stack-item.cw-expanded,
.is-stacked .grid-stack-item.cw-expanded > .grid-stack-item-content {
  position: fixed !important;
  min-height: 0;
}
/* An expanded tile must not keep a column slot it is no longer standing in. */
.is-stacked .dashboard.grid-stack > .grid-stack-item.cw-expanded { grid-column: 1 / -1; }

/* The page itself scrolls; the pager still swipes between pages horizontally. */
.is-stacked .pager-page {
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* The dock is a grid row of its own, so nothing has to be reserved here. */
  padding-bottom: 8px;
  scrollbar-width: none;
}
.is-stacked .pager-page::-webkit-scrollbar { width: 0; height: 0; }

/* ── Topbar, compact ─────────────────────────────────────────────────────── */
/* One shrinking row rather than three stacked ones: nine buttons and a clock do
   not fit across 390px, and stacking them eats a tenth of the screen before a
   single widget appears. */
.is-phone .topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 0;
  padding: 6px 8px;
  /* NOT `overflow-x: auto`. A scrolling bar hides its own ends: on a real phone
     the first version cut the leading digit off the clock and the trailing
     characters off the status chip, with no sign that anything was scrollable.
     A flex row that lets its children shrink shows less, but shows all of it. */
  overflow: hidden;
}
/* Only the FLEXIBLE part may shrink. `min-width: 0` on every child was the
   previous rule and it produced the failure it was meant to prevent: with
   nothing allowed to overflow and everything allowed to shrink, the bar shares
   its shortfall out among all of them and clips each one a little. Measured at
   390px: the two quick buttons had 47px for 76px of content, so the second one
   was sliced down its middle, and the SDK and Claude chips were squeezed to
   23px of a 104px chip — a sliver of a letter each. The clock's leading digit
   went with them, because a squashed neighbour still paints over where it sat.
   A control is either legible or absent; there is no useful 22% of a button. */
.is-phone .topbar > * { min-width: auto; flex: 0 0 auto; }
.is-phone .topbar .clock { flex: 0 1 auto; min-width: 0; }
.is-phone .topbar .quickbar > *,
.is-phone .topbar .top-actions > * { flex: 0 0 auto; }
/* Real font sizes, not `transform: scale()`. A transform shrinks the pixels and
   leaves the LAYOUT box alone, so the bar stayed 105px tall and ate a tenth of
   the screen while looking small (measured). The clock also folds onto one line
   here — the phone already shows the time in its own status bar, so this one is
   context, not the headline it is on a dedicated screen. */
.is-phone .topbar .clock {
  justify-self: center;
  min-width: 0;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.is-phone .topbar .clock-h,
.is-phone .topbar .clock-m { font-size: 24px; }
.is-phone .topbar .clock-sep { font-size: 19px; }
.is-phone .topbar .clock-meta { font-size: 10px; gap: 5px; padding: 0; min-width: 0; }
/* The weather is the one meta item that stays, so it is the one that may
   ellipsis — a shortened city name still reads as a city name. */
.is-phone .topbar .clock-weather {
  padding: 2px 6px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Now-playing is one tile away, on a screen with room for it. The SDK badges
   and the Claude chip join it for the same reason and a stronger one: both are
   ~105px of chip, and after two quick buttons, the clock and the weather a
   390px bar has about 100px left for the pair of them. Each has its own widget
   on a screen that can show it whole. */
.is-phone .topbar .clock-media,
.is-phone .topbar .clock-sdkbadges,
.is-phone .topbar .clock-claude,
.is-phone .topbar .clock-date { display: none; }
.is-phone .topbar .quickbar,
.is-phone .topbar .top-actions { gap: 4px; flex-wrap: nowrap; }
.is-phone .topbar .topbtn { min-width: 34px; height: 34px; padding: 0 8px; font-size: 12px; }
/* Free the width these cost: the dock at the bottom carries the same controls
   within thumb reach, and the app-favourites row needs more room than a phone
   has. Layout editing is hidden because a 24-column drag is not a thumb
   gesture, and because the grid is not being drawn at its real geometry here —
   a drag would move a tile somewhere the user cannot see. */
.is-phone .topbar .app-favorites,
.is-phone .topbar .qbtn-layout,
.is-phone .topbar .qbtn-settings,
.is-phone .topbar .qbtn-apps,
.is-phone .topbar .qbtn-search,
.is-phone .topbar .topbtn-xenon { display: none; }
/* The page dots move to the dock's counter; keeping both would be two truths. */
.is-phone .topbar .pager-dots { display: none; }

/* ── Topbar, tablet ──────────────────────────────────────────────────────── */
/* The bar is a three-column grid (quick actions | clock | actions) and it has
   no answer for a width the three cannot share. Measured at 768px: the middle
   column took 683 of 740px and the outer two were computed to ZERO, so the four
   quick buttons wrapped into a vertical stack and the bar grew to 176px tall —
   a quarter of an iPad's height, before a single widget. The date lost its
   first letters to the clipped column and the media strip's title was 22px
   wide. One flex row instead, where the controls keep their size and only the
   clock gives ground. */
.is-tablet .topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 0;
  padding: 6px 10px;
  overflow: hidden;
}
.is-tablet .topbar > * { min-width: 0; flex: 0 0 auto; }
.is-tablet .topbar .clock { flex: 1 1 auto; min-width: 0; justify-self: start; }
.is-tablet .topbar .quickbar,
.is-tablet .topbar .top-actions { flex-wrap: nowrap; }
.is-tablet .topbar .quickbar > *,
.is-tablet .topbar .top-actions > * { flex: 0 0 auto; }
/* The two widest passengers, and the two with a better home on this screen: the
   favourites row wants a desktop's worth of width, and the now-playing strip is
   a tile away. Dropping them is what buys the rest of the bar its natural size
   instead of a share of a shortfall. */
.is-tablet .topbar .app-favorites,
.is-tablet .topbar .clock-media { display: none; }
.is-tablet .topbar .clock-meta > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The Dynamic Island is turned OFF in phone view by topbar-minimal.js, which is
   the right place for it — restyling a pill and two edge rails for a 390px
   screen is fixing the wrong thing. These rules are the belt to that braces:
   they cover the frame where the island is being torn down, and the case of a
   surface that somehow keeps it. The class is `edge-rail`, not `topbar-rail` —
   the first version guessed the name and hid nothing, which is how the rails
   ended up sitting over the widgets on a real phone. */
.is-stacked .edge-rail,
.is-stacked .topbar-mini { display: none !important; }

/* ── Widget internals that assume a desktop tile ─────────────────────────── */

/* System stat cards. `.system-grid` distributes its height with
   `grid-auto-rows: minmax(0, 1fr)` and `overflow: hidden`, so on a short tile
   every card shrinks equally — by design, and the design says a card gives up
   its CHART, not its text (`.stat-track.has-spark` shrinks to 6px). That holds
   until the text alone is taller than the share: measured at 390px, the cards
   were 52px against 73px of content, so RAM's "DDR5 6000 MHz — 2x16 GB" and
   DISCO's "NTFS - Fixed" were cut through the middle of the line — the reported
   bug. Here the tile is one of a scrolling column and there is no reason to
   distribute a fixed height at all: the rows take what their content needs and
   `.side-panel`, which is already `overflow: auto`, carries the remainder.
   Left alone on the Edge, which is the short screen the 1fr rule was written
   for. */
.is-stacked .system-grid {
  grid-auto-rows: minmax(min-content, auto);
  overflow: visible;
}
/* And the tile has to be able to SCROLL that remainder, which it could not.
   `.side-panel` is `overflow: auto` in SystemPanel.css, but DashboardLayout.css
   overrides it to `hidden` for the dashboard, on the reasoning that a desktop
   tile is sized so all four stats fit with no scrollbar. That reasoning does
   not survive the stacked view: the rows take their natural height here and the
   tile's height is capped, so everything past the cap was simply cut off with
   no way to reach it. Reported from a phone: "the System tile doesn't have a
   scroll on mobile (weather etc. does)".
   The selector carries `body:not([data-panel])` on purpose — the rule it has to
   beat has it, and `.is-stacked .side-panel` alone is one point short, so it
   would lose and the fix would look right in the file and do nothing. */
.is-stacked body:not([data-panel]) .side-panel {
  overflow-y: auto;
  overflow-x: hidden;
}
.is-stacked .system-block { flex: 0 0 auto; min-height: min-content; }
.is-stacked .stat-box { min-height: min-content; }

/* Calendar. `minmax(0, 1fr)` rows are right on the Edge, where the tile is tall
   and the month should fill it; here they let a week row fall under the height of
   the digit inside it, and a grid row does not clip its item — it overflows, so
   the numbers were drawn through their neighbours. A floor of 26px keeps a cell a
   cell (and a touch target); the `1fr` still distributes anything above it.
   The card then takes its natural height instead of a share of a short one, and
   `.cal-pane` carries the remainder — which with the `--ph-min` floor above is
   normally nothing, and is a scroll rather than a clip when the user has sized
   the tile smaller still. */
.is-stacked .calendar-days {
  grid-template-rows: repeat(var(--calendar-weeks, 5), minmax(26px, 1fr));
  grid-auto-rows: minmax(26px, 1fr);
}
/* The four tabs above it (Calendario / Task / Timer / Appunti) are one row that
   does not scroll and sits inside `overflow: hidden`, so at 360px the last label
   was cut through — "APPU". A flex item's automatic minimum is its min-content
   width and each label is ONE word, so no button gives ground: what has to give
   is the space around the words. Padding, gap and letter-spacing, in that order,
   because they cost the reader nothing; the type only drops half a point.
   That is enough for every phone from 360px up. Below it, the tabs you are NOT
   on give ground and shorten with an ellipsis — never the active one, which is
   the label that has to keep saying where you are. Applied to all four with
   `min-width: 0` instead, the four share the width equally and the tab you are
   actually on is the first to lose its name. */
.is-stacked .cal-task-btn {
  padding: 6px 6px;
  gap: 4px;
  font-size: 10px;
  letter-spacing: 0.2px;
}
.is-stacked .cal-task-btn:not(.active) { min-width: 0; }
.is-stacked .cal-task-btn:not(.active) span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.is-stacked .cal-pane { overflow-y: auto; }
.is-stacked .cal-pane .calendar-card,
.is-stacked .upcoming-block { flex: 0 0 auto; }

/* ── The thumb dock ──────────────────────────────────────────────────────── */
.ph-dock {
  /* A real row in the shell's grid, not a floating bar. See buildDock() in
     js/phone-view.js for why: nothing can end up underneath a row. */
  display: none;
  align-items: center;
  gap: 8px;
  /* The bar takes a VARIABLE number of buttons (a feature registers one, and
     the transfer widget does), so any fixed width budget is a guess that the
     next feature breaks. Wrapping is the belt to `.ph-acts`'s braces below,
     which is where the real guarantee lives; between them, an overflow costs
     one more row of height instead of quietly pushing a control off screen. */
  flex-wrap: wrap;
  row-gap: 6px;
  margin: 0 -10px;
  padding: 6px 12px calc(6px + env(safe-area-inset-bottom));
  background: rgba(10, 12, 14, 0.86);
  backdrop-filter: blur(14px);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.is-phone .shell { grid-template-rows: auto minmax(0, 1fr) auto; }
.is-phone .ph-dock { display: flex; }
/* Right-anchored, and that is a safety decision rather than a cosmetic one: a
   mobile browser floats its own control over the bottom-RIGHT corner of the
   page (reported on Android 16 — it covered the dock's tail, which used to be
   the Settings forwarder, the only route to the panel on this chrome). The
   pager is the one control here with a second route — the pages still swipe —
   so it is the one that can afford to be covered. See buildDock(). */
.ph-pages {
  display: none;
  align-items: center;
  gap: 2px;
  margin-left: auto;
  flex: 0 0 auto;
}
.ph-dock.has-pages .ph-pages { display: flex; }
.ph-page-at {
  min-width: 30px;
  text-align: center;
  font: 600 0.76rem/1 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  color: var(--text-dim, rgba(255, 255, 255, 0.5));
}
/* Left-anchored, so the forwarders keep the same slots whether or not the pager
   is showing, and Settings lands nearer the middle of the bar than either edge.
   `flex: 1` with `min-width: 0` is what lets the buttons give ground before the
   bar ever overflows — the group has to be allowed to shrink for its children's
   own shrinking to mean anything. */
.ph-acts {
  display: flex;
  gap: 4px;
  /* `flex: 1 1 0`, and the ZERO is the part that matters. With an `auto` basis
     the group's hypothetical width is the sum of its buttons at their natural
     size, which is what a browser decides wrapping on — so five labelled
     buttons would push the pager onto a second line on an ordinary 390px phone
     before anything had a chance to shrink. A zero basis makes the group take
     what is left after the pager and share it out. */
  flex: 1 1 0;
  min-width: 0;
  /* The actual no-overflow guarantee. Below roughly 330px the five buttons
     cannot all keep their 44px minimum on one line, and a flex row that cannot
     wrap resolves that by sliding its tail out of view, silently. Here it takes
     a second line. Nothing in this bar may ever be off screen. */
  flex-wrap: wrap;
  row-gap: 4px;
}
.ph-btn {
  appearance: none;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text, #f2f4f3);
  /* 44px: the smallest target a thumb hits reliably. */
  min-width: 44px;
  height: 44px;
  border-radius: 12px;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.ph-btn:active { background: rgba(255, 255, 255, 0.12); }
.ph-btn:disabled { opacity: 0.3; }

/* ── An action button: the app's own icon, and a word under it ────────────── */
/* The icon is CLONED from the control this forwards to (see dockIcon()), so the
   bar wears the same marks as the topbar instead of Unicode the device font
   decides how to draw. The caption is the other half of the same fix: five
   symbols in a row is a puzzle, five labelled controls is a bar. */
.ph-btn.ph-act {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  position: relative;         /* the mirrored indicator anchors to this */
  /* Share the row evenly and shrink together, so one long caption cannot make
     its own button wider than its neighbours. The max keeps a dock with only
     two buttons from stretching them across half the screen. */
  flex: 1 1 0;
  min-width: 44px;
  max-width: 84px;
  height: 52px;
  padding: 5px 3px 4px;
}
.ph-ico {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  height: 19px;
}
.ph-ico svg {
  width: 19px;
  height: 19px;
  display: block;
  fill: currentColor;
}
/* The glyph fallback, for a surface where the target button (and therefore its
   icon) is not in the document. It is deliberately the exception now. */
.ph-glyph { font-size: 17px; line-height: 19px; }
.ph-lab {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--text-dim, rgba(255, 255, 255, 0.62));
}
.ph-btn.ph-act:active .ph-lab { color: var(--text, #f2f4f3); }
/* A registered action (currently: send files) is the primary thing on the dock,
   so it is the one that reads as a button rather than as chrome. */
.ph-btn-action {
  border-color: rgba(var(--accent-rgb), 0.45);
  background: rgba(var(--accent-rgb), 0.16);
}
.ph-btn-action .ph-lab { color: var(--text, #f2f4f3); }

/* The mirrored indicator. Same look as the topbar's own dot (UpdateModal.css),
   because it means the same thing: it is the SAME state, shown on the copy of
   the button the user can actually see on a phone. */
.ph-dot {
  position: absolute;
  top: 5px;
  right: 7px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #ff4d4d;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.35), 0 0 9px rgba(255, 77, 77, 0.85);
  animation: upd-dot-pulse 2s ease-in-out infinite;
}
.ph-dot[hidden] { display: none; }

/* The pager's steps stay square: a chevron needs no caption, and the two of
   them are what buys the actions their width. */
.ph-btn.ph-step {
  min-width: 38px;
  width: 38px;
  height: 52px;
  font-size: 20px;
  padding: 0;
}

/* Narrow phones (iPhone SE and most Androids at 360px). Only the paddings and
   the gaps give ground — never the 44px minimum, which is the dimension a thumb
   actually misses, and never the caption, which is what the bar is for. If it
   still does not fit, the dock wraps rather than hiding its tail. */
@media (max-width: 420px) {
  .ph-dock { gap: 6px; padding-left: 8px; padding-right: 8px; }
  .ph-acts { gap: 3px; }
  .ph-btn.ph-step { min-width: 34px; width: 34px; }
  .ph-page-at { min-width: 26px; }
}

/* ── Nothing floats over the dock ────────────────────────────────────────── */
/* The dock is a grid ROW, which settles every argument with things the
   dashboard lays out and none at all with the things it FIXES to the viewport.
   There are eight of those, and three of them (the Discord invite, the Game
   Companion pill, the layout button) sit in the bottom-RIGHT corner — the exact
   corner this file spends its longest comment conceding to the browser. On a
   desktop they float over a wide empty margin; on a 390px phone they land on
   the bar. Reported while a game was running, which is when the Game Companion
   pill appears.
   `--ph-dock-h` is written by measureDock() in js/phone-view.js from the real
   element: the dock wraps to a second line on a narrow screen and grows with
   the safe-area inset, so a constant here would be wrong on some phone. The
   fallback is only for the frame before the first measurement.
   When you add something fixed to the bottom of the viewport, add it here —
   test/phone-dock-render.test.mjs scans the component CSS and fails if a
   bottom-anchored floater is missing from this list. */
.is-phone .discord-invite,
.is-phone .gc-pill,
.is-phone .event-toast,
.is-phone .deck-toast,
.is-phone .perf-banner,
.is-phone .perf-toast,
.is-phone .vpet {
  bottom: calc(var(--ph-dock-h, 64px) + 12px);
}

/* ── Things that do not belong on a phone ────────────────────────────────── */
/* Layout editing: the grid is not being shown at its real geometry here, so a
   drag would move a tile somewhere the user cannot see. `.layout-fab` is the
   button that OPENS it and lives outside `.layout-controls`, so hiding the
   controls left the button floating in the corner offering an editor this view
   refuses to show. */
.is-stacked .layout-controls,
.is-stacked .layout-fab,
.is-stacked .gs-edit-overlay,
.is-stacked .pager-page-btn { display: none !important; }
/* The home bar is a native-kiosk gesture strip; on a phone it overlaps the dock. */
.is-phone #xenon-home-bar { display: none; }

@media (prefers-reduced-motion: reduce) {
  .ph-btn { transition: none; }
  /* The mirrored update dot pulses; it still has to be VISIBLE, so the
     animation stops rather than the element being hidden with it. */
  .ph-dot { animation: none; }
}
