/* springback — phone-first. Every list is a stack of tappable rows; nothing scrolls sideways.
   "Anything pretty" is out of scope (SPEC §1); this is the minimum that makes 162 rows readable
   on a phone and the handful that matter impossible to miss. */

:root {
  /* THE BROWSER HAS TO BE TOLD. Everything here is painted dark by hand, but the parts the
     browser draws itself — a <select>'s chevron, scrollbars, focus rings, the caret — follow
     color-scheme, which defaults to light. So the account picker had a black arrow on a black
     field: invisible, and the only clue that the row was a menu at all. Reported.
     One line, and it covers every native control rather than each one being restyled. */
  color-scheme: dark;

  --bg: #12100e;
  --panel: #1c1917;
  --panel-2: #262220;
  --line: #37312d;
  --ink: #ece7e1;
  --dim: #a09890;
  --accent: #d98c3f;
  --danger: #e0573e;
  --radius: 10px;
  /* 44px is Apple's minimum comfortable touch target, and this is a phone-first UI. */
  --tap: 44px;
  --header-h: 113px;
  /* THE READABLE COLUMN. One measure for the whole page: the header, the tabs, the banners,
     the job strip and the content all line up on it. Without this the bar spanned the window
     while the content sat in a 760px column, so on a wide screen the title was hundreds of
     pixels left of the heading underneath it. */
  --measure: 760px;
  --gutter: 16px;
}

* { box-sizing: border-box; }

/* NOTHING MAY SCROLL THE PAGE SIDEWAYS. One element wider than the viewport drags the whole
   layout right and cuts the left edge off every line — the title, the labels, the start of every
   sentence — which is what a table of six columns did on a phone. The tables are gone, but this
   is the guard that keeps the next wide thing from doing it again: a horizontal scrollbar on the
   document is never what this UI wants, so it is disallowed at the root rather than avoided by
   discipline in each component. */
/* `overflow-x: CLIP`, on the body only — NOT `hidden`, and not on <html>.
   `overflow-x: hidden` makes the element a scroll container, and doing that to the document is
   what produced iOS Safari's "blank until I scroll" on a back-swipe: restoring a scroll offset
   into a container the browser no longer believes is the scroller leaves an unpainted viewport.
   `clip` does the same clipping with no scroll container and no effect on scroll restoration. */
html, body {
  max-width: 100%;
}
body { overflow-x: clip; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* Bundle ids and email addresses have no spaces to break at, so they must be allowed to break
   anywhere rather than setting a minimum width the viewport cannot meet. */
.row-sub, .fact dd, .job-line, .job-detail { overflow-wrap: anywhere; }

header {
  /* NO SIDE PADDING ON THE BAR ITSELF. The bar spans the window — its background and its rule
     should reach both edges — while its CONTENTS sit in the column, which is what .titlebar and
     nav do below.
     THE TOP INSET IS NOT DECORATION under viewport-fit=cover: the page now owns the status bar
     and the notch, so without it the title sits under the clock. The height is measured in JS and
     reclaimed as body padding, so growing it here needs no second change. */
  padding: calc(14px + env(safe-area-inset-top, 0px)) 0 0;
  border-bottom: 1px solid var(--line);
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  background: var(--bg);
  z-index: 5;
}
/* NO COMPOSITOR PROMOTION HERE, AND THAT IS THE FIX RATHER THAN AN OMISSION.
   I added `transform: translateZ(0); will-change: transform` to stop a repaint flicker, and a
   screen recording showed it caused a worse one: for a single frame during a back-swipe the
   header vanished entirely and the list was drawn at the top of the screen. A sticky element on
   its own layer can be composited with the page's new scroll offset applied but its sticky
   adjustment not yet recomputed — so it is drawn at its static position, hundreds of pixels
   above the viewport, and simply is not there. Sticky and forced layer promotion do not mix. */
/* max() rather than a sum: the gutter is already generous, and a notch in landscape only needs
   more where it actually intrudes. On a phone with no insets these resolve to the gutter exactly
   as before. */
.titlebar, nav {
  max-width: var(--measure);
  margin-left: auto;
  margin-right: auto;
  padding-left: max(var(--gutter), env(safe-area-inset-left, 0px));
  padding-right: max(var(--gutter), env(safe-area-inset-right, 0px));
}
.titlebar { display: flex; align-items: center; gap: 10px; }
h1 { margin: 0; font-size: 19px; letter-spacing: -0.01em; }
.tagline { margin: 1px 0 10px; color: var(--dim); font-size: 13px; }
/* HIDDEN ON A PHONE, and hidden by default rather than by a max-width query, so a browser that
   never matches the desktop rule below errs towards the smaller header. The gate keeps its own —
   see .gate-card .tagline, which this does not touch. */
.titlebar .tagline { display: none; }


nav { display: flex; gap: 2px; overflow-x: auto; }
nav button {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--dim);
  padding: 9px 12px;
  font: inherit;
  font-size: 15px;
  cursor: pointer;
  min-height: var(--tap);
  white-space: nowrap;
}
nav button.active { color: var(--ink); border-bottom-color: var(--accent); }

main {
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--gutter);
  /* THE SAME THREE INSETS AS THE HEADER AND THE TOAST, for the same reason: under
     viewport-fit=cover the page owns the whole screen, so the last row of a long list would
     otherwise end up under the home indicator and the first column under a landscape notch.
     max() keeps the ordinary gutter on a device with no insets. */
  padding-left: max(var(--gutter), env(safe-area-inset-left, 0px));
  padding-right: max(var(--gutter), env(safe-area-inset-right, 0px));
  padding-bottom: calc(var(--gutter) + env(safe-area-inset-bottom, 0px));
}

.banner {
  max-width: calc(var(--measure) - var(--gutter) * 2);
  width: calc(100% - var(--gutter) * 2);
  margin: 10px auto 0;
  padding: 10px 12px;
  border-radius: var(--radius);
  font-size: 14px;
}
.banner.warn { background: #3a2c14; border: 1px solid #6b4f1d; }

/* --- the gate ------------------------------------------------------------ */

/* WHILE GATED, THE APP IS NOT MERELY COVERED — it is display:none. An overlay alone would leave
   the header fixed to the viewport on top of the login card, and would keep a screenful of
   device names on screen behind a semi-transparent panel after a session expired. */
body.gated > header,
body.gated > main,
body.gated > #jobs,
body.gated > #fake-banner,
body.gated > #insecure-banner { display: none; }

#gate {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
  background: var(--bg);
}
.gate-card { width: 100%; max-width: 360px; }
.gate-card h1 { margin: 0; font-size: 24px; }
.gate-card .tagline { margin: 2px 0 14px; }
.gate-card .banner { margin: 0 0 14px; }
.gate-card .hint { margin: 0 0 14px; }

/* A readonly field must not look like one you are expected to fill in — but it also must not
   look broken. Dimmed, no caret, and it still hands the password manager the anchor it wants. */
#gate-user { color: var(--dim); cursor: default; }

/* --- jobs strip ---------------------------------------------------------- */

/* AN EMPTY JOBS STRIP IS NOT A BOX. It used to be a sticky, positioned, zero-height element
   sitting between the sticky header and the content at all times — so every navigation had two
   stacked sticky boxes to resolve, and the header flickered for a frame on the way back.
   Removed from the layout entirely when there is nothing in it. */
#jobs:empty { display: none; }
#jobs { position: sticky; top: var(--header-h); z-index: 4; padding: 10px 0 0; }
.job {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin: 0 auto 8px;
  /* The same box the banners get: the measure less both gutters, so its edges line up with the
     content underneath rather than sitting 16px proud of it. */
  max-width: calc(var(--measure) - var(--gutter) * 2);
  width: calc(100% - var(--gutter) * 2);
}
.job-line { display: flex; justify-content: space-between; gap: 10px; font-size: 14px; }
.job-pct { color: var(--accent); font-variant-numeric: tabular-nums; white-space: nowrap; }
.job-detail { color: var(--dim); font-size: 12.5px; margin-top: 4px; font-variant-numeric: tabular-nums; }
.bar { height: 5px; background: var(--panel-2); border-radius: 3px; margin-top: 7px; overflow: hidden; }
.bar-fill { height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.4s ease; }

/* --- rows ---------------------------------------------------------------- */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: 10px;
  overflow: hidden;
}

.list { display: flex; flex-direction: column; }

/* A row is a button so it is tappable, focusable and keyboard-reachable for free. */
.row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 10px 12px;
  background: none;
  border: 0;
  border-bottom: 1px solid #241f1d;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.row:last-child { border-bottom: 0; }
/* HOVER ONLY WHERE THERE IS A POINTER. On a touch screen there is no such thing as "no longer
   hovering", so a tapped row keeps the highlight after the page it opened has been swiped away —
   which reads as a selected row on a list that has no selection. The tap highlight goes for the
   same reason: iOS paints its own grey box and leaves it behind through a back gesture. */
@media (hover: hover) and (pointer: fine) {
  .row:hover:not(:disabled) { background: #211d1b; }
}
.row { -webkit-tap-highlight-color: transparent; }
/* Keyboard focus still has to be visible — this removes a stuck HOVER, not the focus ring. */
.row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.row:disabled { cursor: default; opacity: 0.55; }
.row.static { cursor: default; }

.row-main { flex: 1; min-width: 0; }
.row-title {
  font-weight: 500;
  font-size: 15.5px;
  /* Long store names must truncate rather than push the status chip off screen. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-sub {
  color: var(--dim);
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.row-right { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.chev { color: var(--dim); font-size: 22px; line-height: 1; }
.tick { color: var(--dim); font-size: 12.5px; white-space: nowrap; }

.pill {
  font-size: 11.5px;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--dim);
  white-space: nowrap;
}
.pill.offline { border-color: #5b4a2e; color: #c9a66b; }
.pill.live { border-color: #40563a; color: #9dbb8a; }
.pill.risk { border-color: #7a3427; color: #f0a08e; background: #2e1a15; }
/* Not paired: needs an action, so it reads as louder than `offline` — which needs nothing but
   patience — without borrowing the red that `risk` uses for an app about to be lost for good. */
.pill.unpaired { border-color: #7a5a2a; color: #e0ae63; background: #2b2113; }

.status { font-size: 11.5px; padding: 2px 7px; border-radius: 4px; white-space: nowrap; }
.status.delisted { background: #4a1f16; color: #ffb9a6; font-weight: 600; letter-spacing: 0.03em; }
.status.available { background: #23301f; color: #9dbb8a; }
.status.unknown { background: #2f2a24; color: #c0b3a2; }
/* A never-public listing is not a finding — muted, so it cannot be mistaken for one. */
.status.not_listed { background: #262220; color: #8d857c; }

/* The body has no padding of its own, because the rows inside it run edge to edge — their own
   separators are meant to span the card. Anything that is NOT a row (the loading line, an error)
   has to supply the inset itself, or it sits flush against the card edge while every row above
   it is indented. */
.device-body { border-top: 1px solid var(--line); padding: 0 0 4px; }
.device-body > p,
.device-body > .error {
  margin: 0;
  padding: 14px 12px;
}
.device-body > .error { margin: 12px; padding: 10px 12px; }

.summary { padding: 14px 12px 10px; }
.summary-line { margin: 0; font-size: 15px; }
.summary strong { color: var(--accent); }
.sub { color: var(--dim); font-size: 12.5px; margin: 5px 0 0; }

/* --- detail -------------------------------------------------------------- */

.detail-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.detail-head button.link { margin-left: auto; }
.detail-head h2 { margin: 0; }

.detail-hero { display: flex; align-items: center; gap: 14px; }
/* The heading block already carries its own bottom margin; inside the hero it would push the
   icon's baseline off centre. */
.detail-hero .detail-head { margin-bottom: 0; }
.detail-hero { margin-bottom: 16px; }

/* --- app icons ----------------------------------------------------------- */

/* THE TILE IS SIZED IN CSS AND NEVER BY THE IMAGE. Bundles ship icons at whatever size they
   like — 120px here, 152px there, occasionally 180 — and letting the intrinsic size through
   would give a list of rows with different heights that settle one by one as each PNG arrives.
   Fixed box, object-fit, no reflow, ever.

   The rounded square is iOS's "squircle" approximated with a plain radius. The real superellipse
   needs an SVG mask, which is a lot of machinery to be slightly less wrong at four corners. */
.app-icon {
  position: relative;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--panel-2);
  border: 1px solid var(--line);
}
.app-icon-sm { width: 38px; height: 38px; border-radius: 9px; }
.app-icon-lg { width: 68px; height: 68px; border-radius: 16px; }

/* The monogram sits UNDER the image and shows through until one loads — so an app with no icon
   in its archive looks intentional rather than broken, and one whose icon is still arriving does
   not flash an empty hole. */
.app-icon-letter { color: var(--dim); font-weight: 600; user-select: none; }
.app-icon-sm .app-icon-letter { font-size: 17px; }
.app-icon-lg .app-icon-letter { font-size: 30px; }

.app-icon-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.18s ease;
}
.app-icon-img.loaded { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .app-icon-img { transition: none; }
}

.facts {
  margin: 0 0 16px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
.fact {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  padding: 9px 12px;
  border-bottom: 1px solid #241f1d;
  font-size: 14px;
}
.fact:last-child { border-bottom: 0; }
.fact dt { color: var(--dim); flex-shrink: 0; }
.fact dd {
  margin: 0;
  text-align: right;
  word-break: break-word;
  font-variant-numeric: tabular-nums;
}

.actions-block { margin: 16px 0; display: flex; flex-direction: column; gap: 10px; }
/* A text button in a column of stretched controls would be a full-width invisible button with its
   label floating in the middle. Shrink it to its text and put it where a link belongs. */
.actions-block button.link { align-self: flex-start; padding-left: 0; }

/* --- generic ------------------------------------------------------------- */

button.primary, button.danger, button.secondary, button.link {
  border-radius: 8px;
  padding: 11px 14px;
  font: inherit;
  font-size: 15px;
  cursor: pointer;
  min-height: var(--tap);
}
button.primary { background: var(--accent); border: 1px solid var(--accent); color: #1a1108; font-weight: 600; }
button.danger { background: var(--panel-2); border: 1px solid var(--line); color: var(--danger); }
/* A NEUTRAL full-width button. Sign out had no class at all, so `button.wide` gave it a width and
   the browser gave it everything else — a 20px-tall native control stretched across the screen. */
button.secondary { background: var(--panel-2); border: 1px solid var(--line); color: var(--ink); }
/* A neutral text button. `.link` alone is the danger red used by Remove and Delete, which is
   the wrong signal for something as harmless as re-reading a page. */
button.link.plain { color: var(--dim); }
button.link.plain:hover { color: var(--ink); }
button.link {
  background: none;
  border: 0;
  color: var(--danger);
  font-size: 13.5px;
  padding: 6px 4px;
  min-height: auto;
}
button.wide { width: 100%; }
button:hover:not(:disabled) { filter: brightness(1.1); }
button[disabled] { opacity: 0.45; cursor: default; }

/* The action under a list, and the sentence explaining its limits. Left-aligned with the rows
   above it — the button's own 4px of padding is cancelled so the two edges line up. */
.list-foot { margin-top: 12px; display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.list-foot button.link { padding-left: 0; }
.list-foot .hint { margin: 0; max-width: 60ch; }

/* A rare, hard-to-undo action — Unpair — kept as small print under the state it belongs to,
   rather than as a full-width button competing with Pair. */
.quiet-actions { margin: 2px 0 4px; }
.quiet-actions button.link { padding-left: 0; }

/* A control inside a facts row: the Wi-Fi switch sits beside its value, not under a heading. */
/* An action inside a facts row — "Unpair", "Turn off" — sitting beside the value it changes.
   ANY .link.inline, NOT just .plain.inline: Unpair is `link danger inline` and never matched the
   earlier selector, so it kept the base button's 6px/4px padding and its own font size. That is
   what put it a pixel off the baseline of the text next to it and gave it a different gap from
   the Wi-Fi row's. Reported.

   font-size and line-height INHERIT so the two sit on one baseline by construction rather than by
   a nudge, and the margin is the gap — padding would push the underline out with it. */
button.link.inline {
  font-size: inherit;
  line-height: inherit;
  padding: 0;
  min-height: 0;
  margin-left: 12px;
  vertical-align: baseline;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.dim { color: var(--dim); }

.empty { color: var(--dim); padding: 20px 0; }
.hint { color: var(--dim); font-size: 13px; }
.hint.spaced { margin-top: 18px; }
.note {
  background: #1d2416;
  border: 1px solid #3b4a2e;
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 13.5px;
}
.note.warn-note { background: #2e1a15; border-color: #7a3427; }
/* A NEUTRAL note, for something that is neither reassuring nor alarming. The green above says
   "nothing is wrong here", which is right for an app that never had a store listing and wrong
   for a capability that is switched off — that should be noticed, not soothed. */
.note.plain { background: var(--panel); border-color: var(--line); }
.error {
  background: #2e1a15;
  border: 1px solid #7a3427;
  border-radius: var(--radius);
  padding: 10px 12px;
  font-size: 14px;
  margin: 8px 0;
}

h2.screen { font-size: 19px; margin: 0 0 3px; }
h3.sub-head { font-size: 15.5px; margin: 22px 0 6px; }
p.screen-hint { color: var(--dim); font-size: 13.5px; margin: 0 0 14px; }

form.stack { display: flex; flex-direction: column; gap: 12px; }
label { display: block; font-size: 13px; color: var(--dim); }

/* `label { display: block }` OUTRANKS THE HTML `hidden` ATTRIBUTE, and that is a bug I shipped.
   `hidden` works only through the user-agent stylesheet's `[hidden] { display: none }`, which any
   author rule for the same element beats. So the verification-code field — a <label> marked
   hidden — was visible from the moment the page loaded, asking for a 2FA code before a password
   had even been typed.

   It very likely also broke Safari's keychain autofill, which was the other half of the report: a
   form showing username + password + one-time-code all at once is not a login form to a password
   manager, it is a verification form, so the saved password was never offered.

   Restated globally with !important so no future element rule can quietly defeat `hidden` again. */
[hidden] { display: none !important; }

/* 16px ON PHONES, 13px FROM 640px UP — and the 16px is load-bearing, not a style preference.

   iOS Safari zooms the whole page in when a focused control computes below 16px: WebKit's target
   scale is `16 / fontSize`, so a 13px field was a 1.23x zoom and a re-scroll on every tap — on the
   Apple-ID password field, the 2FA code field and the App Store id field, which are the only text
   inputs this app has. the identical bug has been measured elsewhere; this is the same
   fix in plain CSS.

   DO NOT "fix" this in the viewport meta instead. `maximum-scale=1` / `user-scalable=no` also
   stops the zoom and disables pinch-zoom for the whole app — WCAG 1.4.4. `index.html` is
   deliberately correct as it stands. */
input, select {
  display: block;
  width: 100%;
  margin-top: 5px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink);
  padding: 10px;
  font: inherit;
  font-size: 16px;
  min-height: var(--tap);
}

@media (min-width: 640px) {
  input, select { font-size: 15px; }
  /* The pitch comes back where the header is not competing with the content for a phone screen.
     Only in the header — the gate's copy of it is never hidden. */
  .titlebar .tagline { display: block; }
}

/* The toast floats over the page, so whatever is at the bottom is hidden while it shows. Every
   list therefore ends with room for it — otherwise the last row of a device list, or the last
   action on a detail screen, is unreachable for as long as a message is up. */
main { padding-bottom: 96px; }

#toast {
  position: fixed;
  /* CLEAR OF THE HOME INDICATOR, AND OF A NOTCH IN LANDSCAPE. On a phone with no home button the
     bottom ~34px belongs to the system, and a bar pinned at `bottom: 16px` sits under it — the
     last line of a long error was being crossed out by the indicator.
     THIS RULE USED TO BE INERT. env() reports 0 unless the page asks for viewport-fit=cover, and
     the meta tag did not — so the value added nothing and the clearance was really Safari's own
     viewport inset, which disappears when its bottom toolbar collapses under a scrolling page.
     The meta tag opts in now, so these are the insets that actually apply. */
  left: max(12px, env(safe-area-inset-left, 0px));
  right: max(12px, env(safe-area-inset-right, 0px));
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  margin: 0 auto;
  max-width: 560px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 12px 14px;
  font-size: 14px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.55);
  z-index: 20;

  display: flex;
  align-items: flex-start;
  gap: 10px;
}
#toast.bad { border-color: #7a3427; }
/* An error can be long and is worth copying, so it wraps and the text is selectable — the id
   and the tool's own words are what you would paste into an issue. */
#toast-text { flex: 1; min-width: 0; overflow-wrap: anywhere; -webkit-user-select: text; user-select: text; }
#toast-close {
  flex-shrink: 0;
  background: none;
  border: 0;
  color: var(--dim);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  /* A comfortable target without making the bar taller: the padding is pulled back out with a
     negative margin, so the × is 44px to a thumb and 20px to the layout. */
  padding: 12px;
  margin: -12px -12px -12px 0;
  min-width: var(--tap);
  min-height: var(--tap);
}
#toast-close:hover { color: var(--ink); }

.spinner::after {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-left: 8px;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: -1px;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* --- affordances ---------------------------------------------------------

   The install control was a `pill` reading "install" — the same shape used elsewhere for
   PASSIVE status ("reachable", "offline"), so it read as a label rather than a control and got
   tapped twice. A thing you can tap must not look like a thing that merely reports. */

/* A REAL BUTTON, not a span inside a clickable row. The row used to be the button, so a tap
   anywhere on the cell started an install; now this is the only thing that does. It keeps its own
   tap target — min-height matches the rest of the app's controls. */
.btn-inline {
  background: var(--accent);
  color: #1a1108;
  font: inherit;
  font-weight: 600;
  font-size: 13.5px;
  padding: 7px 14px;
  min-height: 34px;
  border: 0;
  border-radius: 7px;
  white-space: nowrap;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .btn-inline:hover:not(:disabled) { filter: brightness(1.1); }
}
/* Acknowledge the tap on the way down, not when the response lands. */
.btn-inline:active:not(:disabled) { transform: scale(0.97); }
.btn-inline:disabled { opacity: 0.55; cursor: default; }
.row.busy .row-title { color: var(--dim); }

.tick.done { color: #9dbb8a; }

/* A small circular progress, drawn with a conic gradient — no SVG, no library. */
.ring {
  --pct: 0deg;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: conic-gradient(var(--accent) var(--pct), var(--panel-2) 0);
  flex-shrink: 0;
}
.ring::before {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: var(--panel);
}
.ring-num {
  position: relative;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* percent < 0: spin, because a ring pinned at zero is indistinguishable from a stalled one. */
.ring.spin {
  background: conic-gradient(var(--accent) 90deg, var(--panel-2) 0);
  animation: spin 0.9s linear infinite;
}

/* "in library" is a SECOND, INDEPENDENT fact about an app, not a qualifier on the store verdict.
   As trailing grey text beside the status chip it read as a fragment of that chip, so it is a
   badge of its own, stacked under it.

   STACKED RATHER THAN SIDE BY SIDE, everywhere. Two badges in a row steal the width the title and
   the sub-line need — measured on the Apps screen, where "on alina-iphone · com.apple.clips" was
   cut to "on alina-iphone · c…" on the one row that had two badges, so the row with the MOST to
   say showed the least.

   ANCHORED TO THE TITLE, which is the part that was inconsistent. The column centres its content,
   so one badge sat at the row's middle and a PAIR centred as a block — putting the first badge
   higher on rows that had two. Scrolling a list of a hundred apps, the DELISTED chips wandered up
   and down as rows gained a second badge. Aligning to the top fixes them in one place and reads
   better besides: the status is a fact about the app, and the app's name is the line it now sits
   beside. */
.row-right.stack {
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  align-self: flex-start;
  /* Optical, not arithmetic: the badge's cap height against the title's. */
  padding-top: 2px;
}

.badge {
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 2px 6px;
  border-radius: 4px;
  white-space: nowrap;
}
.badge.library {
  background: #1c2418;
  border: 1px solid #3b4a2e;
  color: #9dbb8a;
}


/* Rows and tabs that navigate are anchors now, so they need the button reset an <a> does not
   get for free. Anchors were chosen over click handlers deliberately: an ordinary navigation is
   what lets the browser drive its own back gesture, history and scroll restoration. */
a.row, nav a {
  text-decoration: none;
  color: inherit;
  -webkit-tap-highlight-color: transparent;
}
nav a {
  display: inline-flex;
  align-items: center;
  border-bottom: 2px solid transparent;
  color: var(--dim);
  padding: 9px 12px;
  font-size: 15px;
  min-height: var(--tap);
  white-space: nowrap;
}
nav a.active { color: var(--ink); border-bottom-color: var(--accent); }

/* The fixed header is out of flow, so the content needs its height back as padding. */
/* The offset goes on the BODY, not on main: the fake-tools banner and the jobs strip are
   siblings of main, so an offset applied only to main would leave them underneath the fixed
   header. Pushing the whole flow down covers everything the header would otherwise cover. */
body { padding-top: var(--header-h); }

/* The back control belongs to the screen it goes back FROM, not to the app header. Keeping it
   out of the header is what lets the header be genuinely invariant across navigations. */
.detail-back {
  display: inline-flex;
  align-items: center;
  /* A device's name goes in here now, and people name phones whatever they like. Clamp it rather
     than let one push the layout or wrap onto two lines. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  background: none;
  border: 0;
  color: var(--accent);
  font: inherit;
  font-size: 16px;
  padding: 4px 8px 10px 0;
  margin-left: -4px;
  cursor: pointer;
  min-height: var(--tap);
  -webkit-tap-highlight-color: transparent;
}
.detail-back:active { opacity: 0.6; }

/* --- loading skeleton ------------------------------------------------------

   Shaped like the card it becomes. A placeholder of the wrong size only moves the jolt from
   "content appears" to "content resizes", which is the layout-stability point that matters most
   on the screen you land on. */

/* Lighter than the row it sits on. The first version used --panel-2, which is exactly the
   device row's own background, so the placeholder lines were invisible and the skeleton was two
   blank slabs — worse than no skeleton, because it looks like a rendering fault. */
.sk {
  background: linear-gradient(90deg, #35302c 25%, #474039 50%, #35302c 75%);
  background-size: 200% 100%;
  border-radius: 5px;
  animation: sk-shimmer 1.4s ease-in-out infinite;
}
.sk-line { height: 13px; }
.sk-line.sk-sub { height: 10px; margin-top: 7px; }
.sk-pill { width: 74px; height: 18px; border-radius: 999px; flex-shrink: 0; }
/* The app-list skeleton: an icon tile and a status chip, matching the real row so nothing moves
   when half a minute of scanning finally lands. */
/* A value still being fetched, sized like the answer it becomes. Inline-block so it occupies a
   line inside a <dd> that is otherwise text. */
.sk-value { display: inline-block; width: 84px; height: 11px; vertical-align: middle; }
.sk-icon { width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0; }
.sk-chip { width: 62px; height: 16px; border-radius: 4px; flex-shrink: 0; }
.sk-card .row, .sk-list .row { cursor: default; }

@keyframes sk-shimmer {
  from { background-position: 200% 0; }
  to { background-position: -200% 0; }
}

/* A shimmer is decoration; someone who has asked for less motion should get a plain block
   rather than a moving one. */
@media (prefers-reduced-motion: reduce) {
  .sk { animation: none; background: #3a3430; }
}

/* --- search ------------------------------------------------------------- */

.search-wrap { margin: 10px 0 6px; position: relative; }
.search {
  width: 100%;
  /* Room for the clear button, so a long query runs under it rather than behind it. */
  padding-right: calc(var(--tap) - 4px);
  /* 16px OR LARGER, OR iOS ZOOMS. Safari magnifies the page when a focused input's text is
     under 16px, and the page never zooms back out — the same rule the other inputs follow. */
  font-size: 16px;
  padding: 10px 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink);
  min-height: var(--tap);
  -webkit-appearance: none;
  appearance: none;
}
/* NO CUSTOM FOCUS RING. It used to draw an accent-coloured one, which made the search box the
   only input in the app that did not focus the way the platform focuses things: reported as
   "search field selection appears orange whereas other inputs are blue", with the Apple ID field
   beside it wearing Safari's blue.
   Every other input here is left to the browser — the same decision as `color-scheme: dark`, which
   is what makes the controls look native in the first place. The search box now is too. */

/* THE NATIVE CLEAR BUTTON IS HIDDEN, NOT RELIED ON. WebKit draws one for input[type=search] on a
   Mac and none at all on iOS, which is where this app is actually used — so the field had no way
   to be emptied but selecting the text and deleting it one-handed. The button beside it is ours,
   and hiding the native one stops a second ✕ appearing next to it on a desktop. */
.search::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; display: none; }

.search-clear {
  position: absolute;
  right: 0;
  top: 0;
  /* A FULL-HEIGHT TAP TARGET rather than a small glyph: the whole right end of the field is
     pressable, which is what makes it usable with a thumb. The ✕ is only what it looks like. */
  height: 100%;
  width: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.search-clear:hover { color: var(--ink); }
/* Left to the browser as well, so the field and its own button agree with each other. */
.search-clear:focus-visible { outline-offset: -4px; border-radius: var(--radius); }
.search-clear[hidden] { display: none; }
