/* app.css — components, built to the geometry measured off the shipped clients.
 * Every block cites the iOS/Android source it mirrors. See css/tokens.css for the values. */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  -webkit-font-smoothing: antialiased;
}

/* Nunito has no glyph for →, ✓ or ＋ — typing those characters drops them into a
   fallback face mid-sentence. Every such mark on this page is an <svg> from the sprite. */
.i { width: 1em; height: 1em; flex: none; fill: none; stroke: currentColor;
     stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: none; }
a { color: var(--brand); }

/* The one focus ring, taken from the precedent already in this repo
   (extension/chrome/popup.css:225,258,321,405). */
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ============================== Shell ============================== */

.shell { min-height: 100dvh; display: flex; flex-direction: column; }

.topbar {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; gap: var(--sp-s);
  padding: var(--sp-s) var(--gutter);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--outline);
}
.topbar__brand { display: flex; align-items: center; gap: var(--sp-xs); font-weight: 800;
                 font-size: var(--fs-title3); letter-spacing: var(--ls-title3); }
.topbar__spacer { flex: 1; }
.topbar .btn { white-space: nowrap; flex: none; }
/* On a phone the topbar has no room for a sentence — "Disconnect this browser" wrapped to
   three lines and pushed the bar to 130px tall. Below the grid breakpoint the label is
   dropped and the glyph carries it (the accessible name stays on the button either way). */
@media (max-width: 600px) {
  .topbar .btn--label-optional span { display: none; }
  .topbar { gap: var(--sp-xs); }
}

.main { flex: 1; width: 100%; max-width: var(--wide); margin: 0 auto;
        padding: var(--sp-m) var(--gutter) var(--sp-xxl); }
.main--readable { max-width: var(--readable); }

/* ============================== Buttons ==============================
   DesignSystem.swift:585-660 · Components.kt:568-625
   Primary is the CEILING of the height ladder (48). Nothing may be taller. */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-xs);
  border-radius: var(--r-chip);
  text-decoration: none;               /* .btn is used on <a> too (Open original) */
  font-weight: 600; font-size: var(--fs-sub);
  height: var(--h-secondary); padding: 0 var(--sp-m);
  background: var(--brand-soft); color: var(--brand);
  transition: transform var(--dur-snappy) var(--ease-snappy),
              background-color var(--dur-chip) var(--ease-chip),
              opacity var(--dur-chip) var(--ease-chip);
}
.btn:active:not(:disabled) { transform: scale(var(--press-scale)); }
.btn:disabled { opacity: .45; cursor: not-allowed; }

.btn--primary {
  height: var(--h-primary);
  border-radius: var(--r-card);        /* the CARD step, not the chip step */
  padding: 0 var(--sp-xl);
  background: var(--brand); color: var(--on-brand);
  font-weight: 700; font-size: var(--fs-headline);
}
.btn--compact { height: var(--h-chip); padding: 0 var(--sp-s); }
.btn--quiet   { height: var(--h-chip); background: none; color: var(--muted); }
.btn--quiet:hover { color: var(--ink); }
.btn--danger  { background: var(--status-failed-bg); color: var(--status-failed); }
.btn--block   { width: 100%; }
/* The sprite has one arrow and it points right. "Back" reuses it mirrored rather than
   carrying a second, near-identical path. */
.btn--back .i { transform: scaleX(-1); }

/* ============================== Chips ==============================
   Components.kt:212-252,655-680. All THREE of bg/fg/border animate — the border used to
   blink out in a single frame when only the background was transitioned. */

.chiprow { display: flex; gap: var(--sp-xs); overflow-x: auto; padding: var(--sp-xxs) var(--gutter);
           margin: 0 calc(-1 * var(--gutter)); scrollbar-width: none; }
.chiprow::-webkit-scrollbar { display: none; }

.chip {
  flex: none;
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--h-chip); padding: 0 var(--sp-m);
  border-radius: var(--r-chip);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--outline);
  text-decoration: none;               /* .chip is used on <a> for the quick-jump row */
  font-size: var(--fs-sub); font-weight: 600; white-space: nowrap;
  transition: background-color var(--dur-chip) var(--ease-chip),
              color var(--dur-chip) var(--ease-chip),
              border-color var(--dur-chip) var(--ease-chip),
              transform var(--dur-snappy) var(--ease-snappy);
}
.chip .i { width: 14px; height: 14px; }
.chip:active { transform: scale(var(--press-scale)); }
.chip[aria-pressed="true"] {
  background: var(--brand); color: var(--on-brand); border-color: transparent;
}

/* ============================== Status pill ==============================
   DesignSystem.swift `StatusPill` · Components.kt:119-168. Never a button. */

.pill {
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--h-badge); padding: var(--sp-xxs) var(--sp-xs);
  border-radius: var(--r-chip);
  font-size: var(--fs-caption2); line-height: var(--lh-caption2);
  letter-spacing: var(--ls-caption2); font-weight: 700;
}
.pill__dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.pill--ready   { background: var(--status-ready-bg);   color: var(--status-ready); }
.pill--pending { background: var(--status-pending-bg); color: var(--status-pending); }
.pill--failed  { background: var(--status-failed-bg);  color: var(--status-failed); }
/* The dot breathes ONLY while the item is still being worked on. */
.pill--pending .pill__dot { animation: breathe 700ms ease-in-out infinite alternate; }
@keyframes breathe { from { opacity: .35 } to { opacity: 1 } }
@media (prefers-reduced-motion: reduce) { .pill--pending .pill__dot { animation: none; opacity: 1; } }

/* ============================== Text field ==============================
   The only geometry source is Android (MainActivity.kt:2722-2749) — iOS uses SwiftUI
   `.searchable`, which the system draws. Height is 44, NOT the 54 that Material's
   OutlinedTextField default produces (both ControlHeight files call that a deviation). */

.field {
  display: flex; align-items: center; gap: var(--sp-xs);
  height: var(--h-secondary); padding: 0 var(--sp-s);
  background: var(--surface);
  border: 1px solid var(--outline); border-radius: var(--r-card);
  transition: border-color var(--dur-chip) var(--ease-chip);
}
.field:focus-within { border-color: var(--brand); }
.field .i { width: 18px; height: 18px; color: var(--muted); }
.field input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none;
  font-size: var(--fs-body); color: var(--ink);
}
.field input::placeholder { color: var(--muted); }

.textarea {
  width: 100%; min-height: 96px; resize: vertical;
  padding: var(--sp-s);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--outline); border-radius: var(--r-card);
  outline: none; font-size: var(--fs-body); line-height: var(--lh-body);
  transition: border-color var(--dur-chip) var(--ease-chip);
}
.textarea:focus { border-color: var(--brand); }

/* ============================== Feed card ==============================
   TimelineRow.swift:229,263-267,351,358-360 · TimelineRow.kt:184-185,193-194,210,246
   Shadow ONLY, never shadow + 1px border ("flat 2019 box"). */

.feed { display: flex; flex-direction: column; gap: var(--sp-s); list-style: none; margin: 0; padding: 0; }

.card {
  display: flex; gap: var(--sp-s); align-items: flex-start;
  width: 100%; text-align: left;
  padding: var(--sp-s);
  background: var(--surface);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  transition: transform var(--dur-snappy) var(--ease-snappy);
}
.card:active { transform: scale(var(--press-scale)); }

.card__thumb {
  width: 78px; height: 78px; flex: none;
  border-radius: var(--r-card);
  object-fit: cover; background: var(--surface-alt);
}
/* KindTile — drawn when there is no hero. Glyph is 0.40x the side (iOS 64/0.40,
   Android 56/0.42 — web takes iOS's ratio). */
.card__tile {
  width: 78px; height: 78px; flex: none;
  border-radius: var(--r-card);
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--kind) var(--kind-tile-alpha), transparent);
  color: var(--kind);
}
.card__tile .i { width: 31px; height: 31px; }

.card__body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: var(--sp-xxs); }
.card__eyebrow {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-caption2); line-height: var(--lh-caption2);
  letter-spacing: var(--ls-caption2); font-weight: 700;
  color: var(--muted); text-transform: none;
}
.card__eyebrow img { width: 12px; height: 12px; border-radius: 3px; }
.card__title {
  margin: 0; font-size: var(--fs-sub); line-height: 1.3; font-weight: 700; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card__summary {
  margin: 0; font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.card__foot { display: flex; align-items: center; gap: var(--sp-xs); margin-top: 2px; }

/* ============================== Skeleton ==============================
   Motion.swift:232-256 · Motion.kt:315-321 · Components.kt:369-398
   2250ms cycle = 1500ms sweep + 750ms rest; band travels -0.6 -> +1.6 of the width;
   the gradient is FULLY OPAQUE base->highlight->base, so it physically cannot strobe. */

.skel { height: 102px; border-radius: var(--r-card); background: var(--surface);
        box-shadow: var(--shadow-card); padding: var(--sp-s); display: flex; gap: var(--sp-s); }
.skel__block { border-radius: 6px; background: var(--skeleton-base); position: relative; overflow: hidden; }
.skel__thumb { width: 78px; height: 78px; border-radius: 14px; flex: none; }
.skel__col { flex: 1; display: flex; flex-direction: column; gap: 8px; padding-top: 2px; }
.skel__block::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(102deg,
    var(--skeleton-base) 0%, var(--skeleton-highlight) 35%,
    var(--skeleton-highlight) 50%, var(--skeleton-highlight) 65%, var(--skeleton-base) 100%);
  transform: translateX(-60%);
  animation: sweep 2250ms cubic-bezier(.45,0,.55,1) infinite;
}
@keyframes sweep {
  0%   { transform: translateX(-60%); }
  66.7%{ transform: translateX(160%); }   /* 1500ms of 2250ms */
  100% { transform: translateX(160%); }   /* 750ms rest */
}
@media (prefers-reduced-motion: reduce) {
  .skel__block { background: var(--skeleton-flat); }
  .skel__block::after { display: none; }
}

/* Entrance cascade — 45ms per row, capped at 400ms (Motion.swift staggerIn).
 *
 * The animation is an ENHANCEMENT and never owns the resting state. It only runs when the
 * list carries .stagger--in, which app.js adds solely on a first paint of a visible
 * document. Two real failures made that rule necessary, both found by driving the page:
 *
 *  1. `animation-fill-mode: both` pins an element at the FROM state (opacity 0) until the
 *     animation actually starts. In a document that is not being rendered — a background
 *     tab — nothing starts, so the whole feed sits invisible while the DOM insists every
 *     row has opacity 1. Content must never be reachable only through an animation.
 *  2. The settling poll re-renders every 4s. With the animation on the bare child selector
 *     that restarted the whole cascade on every tick, so a library containing one
 *     still-processing item flashed top-to-bottom forever.
 */
.stagger--in > * {
  animation: rise var(--dur-reveal) var(--ease-reveal) both;
  animation-delay: min(calc(var(--i, 0) * var(--stagger-step)), var(--stagger-cap));
}
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ============================== States ============================== */

.empty { text-align: center; padding: var(--sp-xxl) var(--gutter); color: var(--muted); }
.empty__title { color: var(--ink); font-size: var(--fs-title3); font-weight: 700;
                letter-spacing: var(--ls-title3); margin: 0 0 var(--sp-xs); }
.empty__body { margin: 0 auto var(--sp-m); max-width: 42ch; font-size: var(--fs-sub); }

/* An error is SHOWN, never swallowed — the repo's standing rule. */
.banner {
  display: flex; align-items: flex-start; gap: var(--sp-xs);
  padding: var(--sp-s); border-radius: var(--r-card);
  font-size: var(--fs-sub); font-weight: 600;
  background: var(--status-failed-bg); color: var(--status-failed);
  margin-bottom: var(--sp-s);
}
.banner--warn { background: var(--status-pending-bg); color: var(--status-pending); }
.banner--ok   { background: var(--status-ready-bg);   color: var(--status-ready); }
.banner .i { width: 18px; height: 18px; margin-top: 2px; }

.toast {
  position: fixed; left: 50%; bottom: var(--sp-l); transform: translateX(-50%);
  z-index: 60; max-width: min(92vw, 460px);
  display: flex; align-items: center; gap: var(--sp-xs);
  padding: var(--sp-s) var(--sp-m); border-radius: var(--r-card);
  background: var(--ink); color: var(--bg);
  font-size: var(--fs-sub); font-weight: 600;
  box-shadow: var(--shadow-soft);
  animation: rise var(--dur-reveal) var(--ease-reveal) both;
}

/* ============================== Save composer ============================== */

.composer {
  background: var(--surface); border: 1px solid var(--outline);
  border-radius: var(--r-card); box-shadow: var(--shadow-soft);
  padding: var(--sp-m); margin-bottom: var(--sp-l);
}
.composer__row { display: flex; gap: var(--sp-xs); margin-top: var(--sp-s); align-items: center; }
.composer__hint { font-size: var(--fs-caption); color: var(--muted); margin: var(--sp-xs) 0 0; }

/* ============================== Pairing ============================== */

.pair { max-width: 460px; margin: 0 auto; text-align: center; }
.pair__code {
  display: flex; justify-content: center; gap: var(--sp-xs); margin: var(--sp-l) 0;
  font-size: 40px; line-height: 1;
}
.pair__emoji {
  width: 64px; height: 64px; display: grid; place-items: center;
  background: var(--surface-alt); border-radius: var(--r-card);
}
.pair__qr { margin: var(--sp-l) auto; width: 200px; height: 200px;
            background: #fff; padding: var(--sp-s); border-radius: var(--r-card); }
.pair__qr canvas { width: 100%; height: 100%; image-rendering: pixelated; }

/* ============================== Detail / reader ============================== */

.detail__hero { width: 100%; max-height: 320px; object-fit: cover;
                border-radius: var(--r-card); background: var(--surface-alt); }
.detail__title { font-size: var(--fs-title); line-height: var(--lh-title);
                 letter-spacing: var(--ls-title); font-weight: 800; margin: var(--sp-m) 0 var(--sp-xs); }
.detail__meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-xs);
                color: var(--muted); font-size: var(--fs-caption); margin-bottom: var(--sp-m); }
.reader { font-size: var(--fs-body); line-height: 1.7; }
.reader img { max-width: 100%; height: auto; border-radius: var(--r-card); }
.reader pre { overflow-x: auto; background: var(--surface-alt);
              padding: var(--sp-s); border-radius: var(--r-chip); }
.reader table { display: block; overflow-x: auto; border-collapse: collapse; }

/* ============================== Responsive ============================== */

@media (min-width: 600px) {
  .feed--grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
  .feed--grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ============================== Tabs + overflow menu ==============================
   Four tabs, the same four the apps ship. On a phone they collapse to glyph-only and the
   wordmark goes, because "Disconnect this browser" already proved a sentence does not fit
   in this bar. */

.tabs { display: flex; align-items: center; gap: var(--sp-xxs); margin-left: var(--sp-s); }
.tab {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--h-secondary); padding: 0 var(--sp-s);
  border-radius: var(--r-chip);
  color: var(--muted); text-decoration: none;
  font-size: var(--fs-sub); font-weight: 600; white-space: nowrap;
  transition: background-color var(--dur-chip) var(--ease-chip),
              color var(--dur-chip) var(--ease-chip);
}
.tab:hover { color: var(--ink); }
.tab--on { background: var(--brand-soft); color: var(--brand); }
.tab .i { width: 16px; height: 16px; }
.tab__badge {
  min-width: 18px; height: 18px; padding: 0 5px;
  display: inline-grid; place-items: center;
  border-radius: 9px;                       /* a true pill on a COUNT is deliberate: it is
                                               a dot with a number in it, not a chip */
  background: var(--status-failed); color: #fff;
  font-size: 10px; font-weight: 800; line-height: 1;
}

.menu { position: relative; }
.menu__panel {
  position: absolute; right: 0; top: calc(100% + var(--sp-xs));
  z-index: 40; min-width: 240px; max-height: 70vh; overflow-y: auto;
  padding: var(--sp-xs);
  background: var(--surface); border: 1px solid var(--outline);
  border-radius: var(--r-card); box-shadow: var(--shadow-soft);
}
.menu__group + .menu__group { margin-top: var(--sp-xs); padding-top: var(--sp-xs);
                              border-top: 1px solid var(--outline); }
.menu__label { margin: var(--sp-xxs) var(--sp-xs); color: var(--muted);
               font-size: var(--fs-caption2); font-weight: 700;
               letter-spacing: var(--ls-caption2); }
.menu__item {
  display: flex; align-items: center; width: 100%;
  min-height: var(--h-row); padding: 0 var(--sp-s);
  border-radius: var(--r-chip);
  color: var(--ink); text-decoration: none; text-align: left;
  font-size: var(--fs-sub); font-weight: 600;
}
.menu__item:hover { background: var(--surface-alt); }
.menu__item--danger { color: var(--status-failed); }

/* ============================== Pushed-screen header ============================== */

.screenhead { display: flex; align-items: center; gap: var(--sp-s); margin-bottom: var(--sp-m); }
.screenhead__title { margin: 0; font-size: var(--fs-title2); line-height: var(--lh-title2);
                     letter-spacing: var(--ls-title2); font-weight: 800; }

@media (max-width: 860px) {
  .topbar__wordmark { display: none; }
  .tab span { display: none; }
  .tab { padding: 0 var(--sp-s); }
}

/* Eight controls cannot fit in one 320–375px row at a 44px touch target.
   Put the four primary destinations on their own row; never clip the More menu. */
@media (max-width: 600px) {
  .topbar { flex-wrap: wrap; gap: var(--sp-xxs); padding-block: var(--sp-xs); }
  .topbar__brand { min-width: 44px; min-height: 44px; }
  .topbar .target-icon { width: 44px; padding-inline: 0; }
  .tabs { order: 10; flex-basis: 100%; margin-left: 0; }
  .tab {
    flex: 1; min-width: 0; min-height: 48px; height: auto;
    flex-direction: column; justify-content: center; gap: var(--sp-xxs);
    padding: var(--sp-xxs); font-size: var(--fs-caption);
  }
  .tab span { display: inline; }
  .tab .tab__badge { position: absolute; top: 0; right: var(--sp-xxs); display: inline-grid; }
}

/* ============================== Rows + blocks ==============================
   The row ladder is separate from the button ladder: 48 / 56 / 64. A row is NOT a button,
   so the 48px CTA ceiling does not apply to it (that is the rule the search field's 54dp
   on Android also lives under). */

.row {
  display: flex; align-items: center; gap: var(--sp-s);
  width: 100%; min-height: var(--h-row); padding: var(--sp-xs) var(--sp-s);
  background: var(--surface); border-radius: var(--r-card);
  color: var(--ink); text-decoration: none; text-align: left;
  font-size: var(--fs-sub); font-weight: 600;
  box-shadow: var(--shadow-card);
}
.row--2 { min-height: var(--h-row-2line); }
.row__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.row__label { overflow: hidden; text-overflow: ellipsis; }
.row__sub { color: var(--muted); font-size: var(--fs-caption); font-weight: 600; }
.row__right { color: var(--muted); font-size: var(--fs-caption); flex: none; }
a.row:hover, button.row:hover { background: var(--surface-alt); }

.block { margin-bottom: var(--sp-xl); }
.block__head { display: flex; align-items: center; justify-content: space-between;
               gap: var(--sp-s); margin-bottom: var(--sp-s); }
.block__title { margin: 0; font-size: var(--fs-title3); line-height: var(--lh-title3);
                letter-spacing: var(--ls-title3); font-weight: 700; }

/* ============================== Pairing grid + misc ============================== */

/* auto-fill, NOT repeat(8, 1fr). `1fr` is minmax(auto, 1fr) and the cells are
   .target-icon with min-width:44px, so eight of them cannot shrink below
   8*44 + 7*4 = 380px — wider than the 343px a 375px phone has inside the gutters. The
   grid overflowed and took the whole page into horizontal scroll, on the ONE screen a
   new user cannot get past. auto-fill drops to seven columns instead. */
.emojigrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
             gap: var(--sp-xxs); margin: var(--sp-m) 0; }
.emojigrid__cell { font-size: 22px; border-radius: var(--r-chip); background: var(--surface-alt); }
.emojigrid__cell:hover { background: var(--brand-soft); }

.rule { border: 0; border-top: 1px solid var(--outline); margin: var(--sp-xl) 0; }

.quote { margin: 0; padding: var(--sp-s) var(--sp-m); background: var(--surface);
         border-left: 3px solid var(--brand); border-radius: var(--r-chip);
         box-shadow: var(--shadow-card); }
.quote p { margin: 0; font-size: var(--fs-body); line-height: 1.6; }
.quote footer { margin-top: var(--sp-xs); color: var(--muted); font-size: var(--fs-caption); }

.nb__code { display: block; margin: var(--sp-s) auto var(--sp-m); max-width: 60ch;
            padding: var(--sp-xs) var(--sp-s); background: var(--surface-alt);
            border-radius: var(--r-chip); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
            font-size: var(--fs-caption); color: var(--muted); word-break: break-word; }

/* An editable title must LOOK editable on hover/focus, or the affordance is invisible. */
[contenteditable="true"].detail__title:hover { background: var(--surface-alt); border-radius: var(--r-chip); }
[contenteditable="true"].detail__title:focus { outline: 2px solid var(--brand); outline-offset: 4px;
                                               border-radius: var(--r-chip); }

/* A title that is standing in for text the pipeline has not produced yet must not look
   like the item's real name. */
.card__title--placeholder { color: var(--muted); font-weight: 600; font-style: italic; }
