/*
 * Haven - design tokens. THEME 1, "the family": Haven is the third app in the
 * suite with Claude Control and Droplet Dock, and it has to read as one on the
 * same phone. Everything below is a port of Droplet Dock's app.css (which is
 * itself a port of Claude Control's) - same NAMES, same measures, and only the
 * VALUES in this :root block are Haven's own. The token contract and the reasons
 * behind each rule live in _app-shell/web/ and droplet-dock-app/web/app.css;
 * this file keeps the load-bearing notes and points there for the rest.
 *
 * ---------------------------------------------------------------------------
 * WHERE THIS PALETTE CAME FROM.
 *
 * Claude Control's clay palette rotated +245 degrees into violet, saturation and
 * lightness carried across unchanged at every step of the surface ramp - the
 * same move Droplet Dock made (+193 into blue). Only the hue moves, which is
 * what makes it a sibling rather than a lookalike. The ramp is therefore a
 * low-saturation NEUTRAL (H~279, S~20%), not a purple.
 *
 * The accent is the one value NOT taken straight off the rotation: clay at 60%
 * lightness lands at 6.0:1 on its background, but violet at the same lightness
 * is only 3.9:1 (the blue channel carries almost no luminance), so --pri sits at
 * 70% instead. Measured pairs, within 0.6 of the siblings':
 *   --txt on --bg 15.6:1 | --txt-dim 7.6:1 | --txt-faint 4.3:1
 *   --pri on --bg  6.2:1 | --on-pri on --pri 6.3:1
 *
 * Why violet: green collides with --ok, gold with --warn, red with --bad, blue
 * is Droplet Dock's and clay is Claude Control's. It is a placeholder for the
 * hue of Joe's Haven logo when that arrives - change --pri, --on-pri, --pri-soft
 * and --pri-line here and nothing else.
 *
 * DARK ONLY, like both siblings: a control panel run from a phone, often at
 * night. There is no light theme to keep honest.
 * ---------------------------------------------------------------------------
 */

:root {
  --bg:        #120d14;
  --surf-1:    #1b151e;
  --surf-2:    #231927;
  --surf-3:    #2c2132;
  --surf-4:    #372a3d;
  --line:      #3d3044;

  --txt:       #eee4f3;
  --txt-dim:   #ae9cb9;
  --txt-faint: #81718b;

  --pri:       #a182e3;
  --on-pri:    #110330;
  --pri-soft:  rgba(161, 130, 227, .15);
  --pri-line:  rgba(161, 130, 227, .38);

  /* Semantic, and DELIBERATELY NOT ROTATED: green stays green in every app.
     --info re-homed to Droplet Dock's teal - its blue sat too close to violet. */
  --ok:        #74c78d;
  --warn:      #e6b93f;
  --bad:       #f2705f;
  --info:      #86f2e7;

  --ok-soft:   rgba(116, 199, 141, .14);
  --warn-soft: rgba(230, 185, 63, .14);
  --bad-soft:  rgba(242, 112, 95, .14);

  --press:     rgba(255, 255, 255, .09);

  --r-sm:  10px;
  --r-md:  16px;
  --r-lg:  22px;
  --r-xl:  28px;
  --r-pill: 999px;

  --ease: cubic-bezier(.2, 0, 0, 1);
  --fast: 120ms;
  --med:  220ms;
  --slow: 320ms;

  --nav-h:  68px;
  /* How far the FAB reaches up from the bottom of the viewport: its own offset
     (--nav-h + --safe-b + 16) plus its 56px height, plus a 16px breathing gap.
     Anything that scrolls has to reserve this much at the bottom or the FAB
     parks on top of the last card - it covered the final 48px of it, which read
     as the card's outline having gone missing (Joe, 2026-10-13). Derived from
     the FAB's numbers on purpose: change its size or offset and this follows. */
  --fab-clear: calc(var(--nav-h) + var(--safe-b) + 16px + 56px + 16px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
  --content-top: 18px;
  /* Desktop: the phone layout, centred, rather than a second layout. */
  --col: 760px;
}

* { box-sizing: border-box; }

/* NOTHING IS SELECTABLE, because nothing is selectable in an Android app - a long
   press raising Copy/Share over the chrome is the loudest "this is a WebView"
   tell there is. Inputs and the read-only text blocks opt back in below. */
body {
  margin: 0;
  background: var(--bg);
  color: var(--txt);
  font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}
input, textarea, select {
  -webkit-user-select: text; user-select: text; -webkit-touch-callout: default;
}

/* ===========================================================================
 * App chrome. Everything below reads the tokens above and defines no colour of
 * its own.
 * ---------------------------------------------------------------------------
 */

/* THE BAR CENTRES, THE TITLE BASELINES - two demands made of two elements. See
   droplet-dock-app/web/app.css for the 4.2px the single-row version was off by. */
header.bar {
  display: flex; align-items: center; gap: 12px;
  padding: calc(var(--safe-t) + 16px) 16px 0;
}
/* Left: the title row over the status line. Right: the actions, centred against
   the pair (align-items on the bar does that), not against the title alone. */
.bar-main { flex: 1; min-width: 0; }
/* min-height: the row used to be 40px tall because the buttons sat in it; keep
   that rhythm now they are beside it, or the whole header climbs 18px. */
.bar-row { display: flex; align-items: center; gap: 5px; min-height: 40px; }
.bar-acts { display: flex; align-items: center; flex: none; }
.title { display: flex; align-items: baseline; gap: 8px; min-width: 0; line-height: 1; }
/* The mark. An inline SVG until Joe's logo lands (then an <img class="logo"> at
   the same 22px height, exactly as the siblings do it). */
.logo { height: 22px; width: 22px; flex: none; display: block; fill: var(--pri); transform: translateY(-1px); }
header.bar h1 { font-size: 19px; font-weight: 600; margin: 0; letter-spacing: -.01em; }
.ver { font-size: 12px; color: var(--txt-faint); font-variant-numeric: tabular-nums; }
/* App update bar: fixed above the nav, in the way on purpose - it stays until
   the Update button is tapped (Joe, 2026-09-20). */
.updbar {
  position: fixed; left: 16px; right: 16px; z-index: 26;
  bottom: calc(var(--nav-h) + var(--safe-b) + 16px);
  display: flex; align-items: center; gap: 12px;
  padding: 10px 10px 10px 16px; border-radius: var(--r-md);
  background: var(--pri); color: #fff; font-size: 14px; font-weight: 600;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .25);
}
.updbar span { flex: 1; min-width: 0; }
.updbar .btn { flex: none; margin: 0; padding: 8px 14px; background: #fff; color: var(--pri); border: 0; }
.updbar .btn:disabled { opacity: .6; }
.updbar[hidden] { display: none; }
.has-updbar .fab { bottom: calc(var(--nav-h) + var(--safe-b) + 16px + 64px); }
/* ...and the content below it has to clear the FAB's new, higher top edge. */
.has-updbar main { padding-bottom: calc(var(--fab-clear) + 64px); }

.icon {
  background: none; border: 0; color: var(--txt-dim);
  font-size: 24px; line-height: 1; padding: 8px; cursor: pointer;
  border-radius: var(--r-pill);
}
/* 24px glyphs with a gap between them, not 20px shoulder to shoulder - three
   of them read as three targets now (Joe: "make these icons/buttons bigger and
   spread them out a bit"). */
.icon + .icon { margin-left: 6px; }
.icon svg { width: 24px; height: 24px; fill: currentColor; display: block; }
/* A stroked glyph among filled ones: the CSS fill above would paint the circle
   solid, so the stroke class wins it back. 2px on a 24 grid matches Material's
   outline weight next to it. */
.icon svg.stroke { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.icon:active { background: var(--press); }
.icon.spin svg { animation: sp 700ms linear infinite; }
@keyframes sp { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .icon.spin svg { animation: none; opacity: .5; } }

/* CC's .appbar-sub: 13px faint, the number brighter and heavier, one line that
   ellipses rather than wrapping the bar taller. */
.status {
  margin: 2px 0 0; padding: 0;   /* inside the bar now; the bar carries the side padding */
  font-size: 13px; color: var(--txt-faint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.status b { color: var(--txt-dim); font-weight: 500; }
.status.bad { color: var(--bad); }
.status.warn { color: var(--warn); }   /* DNS elsewhere: nothing is down, but visitors are not arriving */

/* The nav is fixed, so the scroll area owes it that height plus the gesture
   inset. overflow-x: clip, never hidden - hidden makes main a scroll container
   and takes the sticky search bar off screen with it. */
main {
  padding: var(--content-top) 16px var(--fab-clear);
  overflow-x: clip;
}
.tab { display: none; }
.tab.on { display: block; }

.sec {
  font-size: 11px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--txt-faint); margin: 22px 0 10px;
}
.sec:first-child { margin-top: 4px; }
.empty { color: var(--txt-faint); font-size: 13.5px; margin: 8px 0; line-height: 1.5; }
.foot  { color: var(--txt-faint); font-size: 12px; margin: 8px 0 0; line-height: 1.5; }
.foot.count { text-align: right; }
.note  { color: var(--txt-faint); font-size: 12.5px; line-height: 1.45; }
.note.bad { color: var(--bad); }

/* A LIST SECTION: heading left, count right, one baseline. CC's .section-label
   measures. */
.sec-row { display: flex; align-items: baseline; gap: 12px; margin: 14px 0 6px; }
.sec-row .sec { margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .8px; }
.sec-row .foot { margin: 0 0 0 auto; flex: none; }

/* ---- key/value rows (the detail sheets) ---- */
.rows { background: var(--surf-1); border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.row { display: flex; gap: 12px; padding: 11px 14px; border-top: 1px solid var(--line); font-size: 13.5px; }
.row:first-child { border-top: 0; }
.row .k { color: var(--txt-dim); flex: none; min-width: 38%; }
.row .v { color: var(--txt); text-align: right; margin-left: auto; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.row .v.mono { font-family: "Roboto Mono", ui-monospace, Consolas, monospace; font-size: 12.5px; word-break: break-all; }
.row .v .btn { margin: -6px 0; }

.warnbox {
  margin: 10px 0 0; padding: 12px 14px; font-size: 13px; line-height: 1.5;
  background: var(--bad-soft); color: var(--bad);
  border: 1px solid var(--bad); border-radius: var(--r-sm);
}
.warnbox .t { font-weight: 600; margin: 0 0 6px; font-size: 13.5px; }
/* A secret handed over once (the WP admin login after a create): its own box,
   accent border, copy buttons - not one line in a wall of output. */
.credbox { margin: 12px 0; padding: 12px 14px; border: 1.5px solid var(--pri); border-radius: var(--r-md); background: var(--pri-soft); }
.credbox .t { font-weight: 600; margin: 0 0 8px; font-size: 13.5px; color: var(--pri); }
.credbox .copyname { margin: 6px 0; }
.credbox .note { margin: 8px 0 0; }
.warnbox p  { margin: 0 0 6px; }
.warnbox p:last-child { margin-bottom: 0; }
.warnbox.warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn); }
.warnbox.info { background: var(--pri-soft); color: var(--pri); border-color: var(--pri-line); }

.meter { height: 5px; border-radius: var(--r-pill); background: var(--surf-3); margin: 10px 0 0; overflow: hidden; }
.meter .fill { height: 100%; background: var(--pri); border-radius: var(--r-pill); }
.meter.warn .fill { background: var(--warn); }
.meter.bad  .fill { background: var(--bad); }

/* ---- cards ----
 * Tonal, no border, no shadow - M3 elevates by TONE. Variants use an inset
 * shadow rather than a border so a card's text never jogs sideways. */
.card {
  display: block; width: 100%; text-align: left; font: inherit; color: inherit;
  background: var(--surf-1); border: 0;
  border-radius: var(--r-md); padding: 14px 14px 16px; margin-bottom: 10px;
}
.card h3, .card-name { font-size: 16px; font-weight: 500; margin: 0; }
.card p  { margin: 4px 0 0; font-size: 13px; color: var(--txt-dim); }
.card.tap { cursor: pointer; transition: background var(--fast) var(--ease); }
.card.tap:active { background-image: linear-gradient(var(--press), var(--press)); }
.can-hover .card.tap:hover { background-image: linear-gradient(var(--press), var(--press)); }
.card.off { box-shadow: inset 0 0 0 1px var(--line); background: none; }
.card.down { box-shadow: inset 0 0 0 1px var(--bad); background: var(--bad-soft); }
/* Off-droplet. A dashed edge rather than a colour: this is not a WARNING about
   the site (it may be perfectly healthy), it is "not one of ours to touch", so
   it must not compete with the red a real outage uses. .down still wins, since
   an external site being down is an outage like any other. */
.card.ext { background: none; box-shadow: inset 0 0 0 1px var(--line); border-left: 2px dashed var(--surf-4); }
.card.ext.down { box-shadow: inset 0 0 0 1px var(--bad); background: var(--bad-soft); }

.card-head { display: flex; align-items: flex-start; gap: 10px; }
.card-main { flex: 1; min-width: 0; }
.card-pills { display: flex; align-items: center; gap: 7px; }

.mark { display: inline-flex; align-items: center; flex: none; }
.mark svg { width: 16px; height: 16px; fill: var(--txt-faint); display: block; }
/* The hexagon is an outline with a hollow middle, so at the same box it reads
   smaller than the WP disc next to it. 5% more, then 1px, then 1px more (Joe, 2026-09-15). */
.mark.hex svg { width: 18.8px; height: 18.8px; }

/* A DOMAIN IS NEVER ELLIPSED: the end of a hostname is the part that tells two
   of them apart. It wraps. */
.card-name.dom { white-space: normal; overflow-wrap: anywhere; }

.card-path {
  margin-top: 4px;
  font: 400 12px/1.45 "Roboto Mono", ui-monospace, Consolas, monospace;
  color: var(--txt-dim);
  word-break: break-all; white-space: normal;
}
.card-sub { margin-top: 2px; font-size: 13px; color: var(--txt-faint); }

.card-facts {
  margin-top: 8px;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 3px 10px;
  font-size: 13px; color: var(--txt-faint);
}
.fact { display: inline-flex; align-items: center; gap: 4px; min-width: 0; }
.fact svg { width: 13px; height: 13px; fill: currentColor; flex: none; }
.fact > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fact.ok { color: var(--ok); }
.fact.warn { color: var(--warn); }
.fact.bad { color: var(--bad); }

/* The status light. The halo is what makes a 10px circle readable at arm's
   length; only a BAD dot pulses, and only its halo. */
.card-head > .dot { width: 10px; height: 10px; border-radius: var(--r-pill); flex: none; margin-top: 6px; background: var(--surf-4); }
.card-head > .dot.ok      { background: var(--ok);   box-shadow: 0 0 0 4px var(--ok-soft); }
.card-head > .dot.unknown { background: var(--warn); box-shadow: 0 0 0 4px var(--warn-soft); }
.card-head > .dot.bad { background: var(--bad); position: relative; }
.card-head > .dot.bad::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  box-shadow: 0 0 0 4px var(--bad-soft);
  animation: downpulse 1.6s var(--ease) infinite;
}
@keyframes downpulse { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .card-head > .dot.bad::after { animation: none; } }

/* The disk/size row: bar takes the slack, the number keeps its width and its
   digits line up down the column so the list reads as a ranking. */
.card-disk { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.card-disk .meter { flex: 1; margin: 0; }
.card-disk .v { flex: none; font-size: 12px; color: var(--txt-faint); font-variant-numeric: tabular-nums; }
/* Less room under a list card than under one of CC's action cards - a card that
   ends in a meter or a path carries its own slack. */
.tab .card { padding-bottom: 10px; }

.tags { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }

/* CC'S PILL, TO THE PIXEL. The NEUTRAL base is the rule: a filled coloured pill
   means something is happening; a label that is merely true is grey. */
.pill {
  flex: none;
  font-size: 11px; font-weight: 600;
  letter-spacing: .4px; text-transform: uppercase;
  padding: 4px 9px; border-radius: var(--r-pill);
  background: var(--surf-3); color: var(--txt-dim);
  white-space: nowrap;
}
.pill.quiet  { background: none; border: 1px solid var(--line); color: var(--txt-faint); padding: 3px 8px; }
.pill.accent { background: var(--pri-soft);  color: var(--pri); }
.pill.ok     { background: var(--ok-soft);   color: var(--ok); }
.pill.warn   { background: var(--warn-soft); color: var(--warn); }
.pill.bad    { background: var(--bad-soft);  color: var(--bad); }
.pill.info   { background: rgba(134, 242, 231, .14); color: var(--info); }

/* ---- segmented control: one question, N answers, exactly one true ---- */
.seg {
  display: flex; gap: 3px; padding: 3px;
  margin-bottom: 12px;
  background: var(--surf-1);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
}
.seg-b {
  flex: 1; padding: 8px 10px;
  border: 0; border-radius: var(--r-pill);
  background: none; color: var(--txt-dim);
  font: inherit; font-size: 14px; cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.seg-b .n { font-size: 12px; color: var(--txt-faint); font-variant-numeric: tabular-nums; margin-left: 6px; }
.seg-b.on { background: var(--surf-4); color: var(--txt); font-weight: 600; }
.seg-b.on .n { color: var(--txt-dim); }
.seg-b:active { background: var(--press); }

/* ---- chips: M3 filter chips, CC's values. flex:none is what makes the row
   scroll instead of crushing the chips. ---- */
.chips {
  display: flex; gap: 8px;
  padding-bottom: 6px;
  overflow-x: auto;
  scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chips + .sec     { margin-top: 11px; }
.chips + .sec-row { margin-top: 7px; }
.chip {
  flex: none;
  height: 34px; padding: 0 14px;
  display: inline-flex; align-items: center; gap: 6px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--txt-dim);
  font: 500 14px/1 Roboto, "Segoe UI", system-ui, sans-serif;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
.chip:active { background: var(--press); }
.can-hover .chip:hover { background: var(--press); }
.chip.on { background: var(--pri-soft); color: var(--pri); border-color: var(--pri-line); }
.chip .n { font-size: 12px; color: var(--txt-faint); font-variant-numeric: tabular-nums; }
.chip.on .n { color: var(--pri); }
/* A chip row used as a section switcher inside a sheet (the site menu has nine
   sections - too many for a segment). Same object, lit one = the open section. */
.chips.sections { margin: 12px 0 4px; }
.chip.bad { color: var(--bad); border-color: rgba(242, 112, 95, .4); }
.chip.bad.on { background: var(--bad-soft); color: var(--bad); border-color: var(--bad); }

/* ---- actions: CC's .acts/.btn block, values verbatim ---- */
.acts { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
.btn {
  flex: 1 1 auto;
  min-width: 92px;
  min-height: 40px;
  padding: 0 16px;
  border: 0;
  border-radius: var(--r-pill);
  background: var(--pri);
  color: var(--on-pri);
  font: 500 14px/1 Roboto, "Segoe UI", system-ui, sans-serif;
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  transition: filter var(--fast) var(--ease), background var(--fast) var(--ease), opacity var(--fast) var(--ease);
}
.btn:active { filter: brightness(.88); }
.can-hover .btn:hover { filter: brightness(1.08); }
.btn:disabled { opacity: .38; cursor: default; filter: none; }
/* Only ONE action per sheet is filled - the one you meant to tap. */
.btn.tonal  { background: var(--surf-3); color: var(--txt); }
.btn.tonal:active { background: var(--surf-4); }
.can-hover .btn.tonal:hover { background: var(--surf-4); filter: none; }
/* Outlined, not filled: a destructive action is not the brightest thing in the
   sheet before you have asked for it. */
.btn.danger { background: transparent; color: var(--bad); box-shadow: inset 0 0 0 1px rgba(242, 112, 95, .4); }
.btn.danger:active { background: var(--bad-soft); }
.can-hover .btn.danger:hover { background: var(--bad-soft); filter: none; }
/* ARMED - the confirming tap is a different button: filled, loud, relabelled. */
.btn.danger.armed { background: var(--bad); color: var(--bg); box-shadow: none; font-weight: 600; }
.btn.danger.armed:active { filter: brightness(.88); }
.btn.ghost { background: transparent; color: currentColor; box-shadow: inset 0 0 0 1px currentColor; opacity: .85; }
.btn.ghost:active { background: var(--press); }
.btn.wide { flex: 1 1 100%; }
.btn.sm { min-height: 32px; min-width: 0; padding: 0 12px; font-size: 13px; flex: none; }
.btn svg { width: 18px; height: 18px; fill: currentColor; }

/* ---- form fields: CC's create-session sheet, stacked label over control ---- */
.xfield { display: flex; flex-direction: column; gap: 6px; padding: 8px 0; }
.xlabel { font-size: 13px; color: var(--txt-dim); font-weight: 500; }
.xlabel .hint { color: var(--txt-faint); font-weight: 400; }
.xinput {
  width: 100%; height: 50px;
  border: 0; outline: 0;
  border-radius: var(--r-sm);
  background: var(--surf-3);
  color: var(--txt);
  /* 16px minimum: below it a WebView zooms the page on focus and stays zoomed. */
  font: 400 16px/1.2 Roboto, system-ui, sans-serif;
  padding: 0 14px;
  box-shadow: inset 0 0 0 1px var(--line);
  -webkit-appearance: none; appearance: none;
}
.xinput:focus { box-shadow: inset 0 0 0 2px var(--pri); }
.xinput::placeholder { color: var(--txt-faint); }
.xinput.code { font-family: "Roboto Mono", ui-monospace, Consolas, monospace; font-size: 14px; }
/* copyName(): the thing to type, above the field that asks for it. Same shell as
   .xinput so the pair reads as "this, into that"; the copy button is a square
   at the right edge, dotted like DO's so it reads as an action, not decoration. */
.copyname { display: flex; align-items: stretch; margin: 8px 0 0; border-radius: var(--r-sm); background: var(--surf-1); box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; }
.copyname code { flex: 1 1 auto; min-width: 0; padding: 13px 14px; font: 500 15px/1.3 "Roboto Mono", ui-monospace, Consolas, monospace; color: var(--txt); overflow-x: auto; white-space: nowrap; -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
.copybtn { flex: none; width: 50px; border: 0; margin: 0; padding: 0; background: var(--surf-3); color: var(--txt-dim); box-shadow: inset 1px 0 0 var(--line); outline: 1px dashed var(--line); outline-offset: -5px; display: grid; place-items: center; cursor: pointer; }
.copybtn:active { background: var(--surf-4); color: var(--txt); }
.copybtn svg { width: 20px; height: 20px; fill: currentColor; }
textarea.xinput { height: auto; min-height: 96px; padding: 12px 14px; line-height: 1.45; resize: vertical; }
textarea.xinput.tall { min-height: 60vh; font-size: 12.5px; line-height: 1.5; white-space: pre; overflow-x: auto; }
select.xinput {
  padding-right: 34px;
  background-image: linear-gradient(45deg, transparent 50%, var(--txt-faint) 50%),
                    linear-gradient(135deg, var(--txt-faint) 50%, transparent 50%);
  background-size: 6px 6px, 6px 6px;
  background-position: calc(100% - 18px) 22px, calc(100% - 12px) 22px;
  background-repeat: no-repeat;
}
select.xinput option { background: var(--surf-3); color: var(--txt); }
.xrow { display: flex; gap: 10px; }
.xrow .xfield { flex: 1; min-width: 0; }

/* An option row with an M3 switch. */
.opt {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 4px; width: 100%;
  cursor: pointer; border: 0; background: none; color: inherit; font: inherit; text-align: left;
  border-radius: var(--r-sm);
  transition: background var(--fast) var(--ease);
}
.opt:active { background: var(--press); }
.can-hover .opt:hover { background: var(--press); }
.opt-txt { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.opt-txt b { display: block; font-size: 15px; font-weight: 500; }
.opt-txt span { display: block; font-size: 13px; color: var(--txt-faint); margin-top: 2px; line-height: 1.4; }
.sw {
  flex: none;
  width: 52px; height: 32px;
  border-radius: var(--r-pill);
  background: var(--surf-4);
  box-shadow: inset 0 0 0 2px var(--line);
  position: relative;
  transition: background var(--med) var(--ease), box-shadow var(--med) var(--ease);
}
.sw::after {
  content: '';
  position: absolute; top: 50%; left: 6px;
  width: 16px; height: 16px; margin-top: -8px;
  border-radius: 50%;
  background: var(--txt-faint);
  transition: transform var(--med) var(--ease), width var(--med) var(--ease),
              height var(--med) var(--ease), margin var(--med) var(--ease), background var(--med) var(--ease);
}
.opt.is-on .sw { background: var(--pri); box-shadow: inset 0 0 0 2px var(--pri); }
.opt.is-on .sw::after { width: 24px; height: 24px; margin-top: -12px; transform: translateX(16px); background: var(--on-pri); }

/* The one-line command field (the Run section) - DD's .ask/.askin. */
.ask { display: flex; gap: 8px; width: 100%; align-items: center; margin-top: 8px; }
.askin {
  flex: 1 1 auto; min-width: 0;
  min-height: 40px; padding: 0 14px;
  border-radius: var(--r-pill);
  background: var(--surf-1); border: 1px solid var(--line);
  color: var(--txt);
  font: 16px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.askin:focus { outline: none; border-color: var(--pri); background: var(--surf-2); }
.askin::placeholder { color: var(--txt-faint); }
.ask .btn { flex: 0 0 auto; min-width: 76px; }

/* ---- read-only text blocks: selectable, overriding body ---- */
.code, .out, .quote {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; line-height: 1.5;
  background: var(--surf-1); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 12px; margin: 8px 0 0;
  overflow-x: auto;
  -webkit-user-select: text; user-select: text; -webkit-touch-callout: default;
}
/* NO max-height and NO overflow-y here: a second vertical scroller inside the
   sheet breaks the dismiss gesture. The sheet grows to full height instead. */
.out { white-space: pre-wrap; overflow-wrap: anywhere; }
.out .ok  { color: var(--ok); }
.out .bad { color: var(--bad); }
.out .dim { color: var(--txt-faint); }
.quote { white-space: pre-wrap; color: var(--txt-dim); font-family: inherit; font-size: 12.5px; }
.cmd {
  margin: 6px 0 0; font: 400 12px/1.45 "Roboto Mono", ui-monospace, Consolas, monospace;
  color: var(--txt-faint); word-break: break-all;
  -webkit-user-select: text; user-select: text;
}

/* ---- the run section at the end of a sheet ---- */
.runsec { margin-top: 22px; }
.runsec > .sec:first-child { margin-top: 0; }
.runstat { font-size: 13px; color: var(--txt-dim); margin: 10px 0 0; font-variant-numeric: tabular-nums; }
.runstat.ok  { color: var(--ok); }
.runstat.bad { color: var(--bad); }
.runstat.warn { color: var(--warn); }   /* the run finished; only the connection to it did not */
.pulse {
  display: inline-block; vertical-align: middle; margin-right: 6px;
  width: 8px; height: 8px; border-radius: var(--r-pill);
  background: var(--pri);
  animation: pulse 1.6s var(--ease) infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: .35; transform: scale(.72); } }
@media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }

/* ---- bottom nav: CC's, ported. The active item's ICON sits in a filled pill;
   pill, glyph and label change together off one .is-on. ---- */
.navbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
  height: calc(var(--nav-h) + var(--safe-b));
  padding-bottom: var(--safe-b);
  background: var(--surf-2);
  display: flex; align-items: stretch;
}
.navitem {
  flex: 1; border: 0; background: transparent;
  color: var(--txt-faint);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; cursor: pointer; padding: 8px 0 6px;
  font: 500 11px/1 Roboto, "Segoe UI", system-ui, sans-serif;
  transition: color var(--fast) var(--ease);
  position: relative;
}
.navitem .ind {
  width: 60px; height: 30px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  background: transparent;
  transition: background var(--med) var(--ease);
}
.navitem svg { width: 22px; height: 22px; fill: currentColor; }
.navitem.is-on { color: var(--pri); }
.navitem.is-on .ind { background: var(--pri-soft); }
.navitem:active .ind { background: var(--press); }

@keyframes view-from-right { from { transform: translateX(14%); } to { transform: none; } }
@keyframes view-from-left  { from { transform: translateX(-14%); } to { transform: none; } }
.tab.from-right { animation: view-from-right var(--med) var(--ease); }
.tab.from-left  { animation: view-from-left  var(--med) var(--ease); }
@media (prefers-reduced-motion: reduce) { .tab.from-right, .tab.from-left { animation: none; } }

/* ---- the FAB: CC's, square 56px, above the nav ---- */
.fab {
  position: fixed;
  right: 16px;
  bottom: calc(var(--nav-h) + var(--safe-b) + 16px);
  z-index: 25;
  width: 56px; height: 56px; padding: 0; border: 0;
  border-radius: var(--r-md);
  background: var(--pri); color: var(--on-pri);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
  transition: transform var(--med) var(--ease), opacity var(--med) var(--ease), filter var(--fast) var(--ease);
}
.fab svg { width: 24px; height: 24px; fill: currentColor; }
.fab:active { filter: brightness(.88); }
.can-hover .fab:hover { filter: brightness(1.08); }
.fab[hidden] { display: none; }

/* ---- detail sheet: a port of _Library/patterns/sheet-dismiss. Every rule is
   load-bearing for the gesture in app.js. ---- */
.scrim {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0, 0, 0, .6);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--med) var(--ease);
}
.scrim.is-on { opacity: 1; pointer-events: auto; }
.scrim[hidden] { display: none; }   /* load-bearing: a class that sets display beats [hidden] */

.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  --sheet-peek: 86vh;
  --sheet-full: calc(100vh - var(--safe-t) - 12px);
  max-height: var(--sheet-peek);
  display: flex; flex-direction: column;
  background: var(--bg);
  color: var(--txt);
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  border-top: 1px solid var(--line);
  transform: translateY(101%);
  transition: transform var(--med) var(--ease), max-height var(--med) var(--ease);
  padding-bottom: var(--safe-b);
}
.sheet.is-on { transform: translateY(0); }
.sheet.is-full { max-height: var(--sheet-full); }
.sheet[hidden] { display: none; }
.sheet.dragging { transition: none; }
.sheet.dragging .sheet-body { overflow: hidden; }

.sheet-grab { width: 32px; height: 4px; border-radius: var(--r-pill); background: var(--surf-4); margin: 12px auto 4px; flex: none; }
.sheet-head { padding: 8px 20px 4px; flex: none; }
.sheet-head h2 { font-size: 16px; font-weight: 600; margin: 0; overflow-wrap: anywhere; }
.sheet-body {
  padding: 12px 20px 28px;
  overflow-y: auto;
  flex: 1;
  min-height: 0;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
body.locked { overflow: hidden; }
/* Padding and nothing else: the bar IS the sheet. */
.sheet-foot { flex: none; display: block; padding: 12px 20px calc(16px + var(--safe-b)); }
.sheet-foot[hidden] { display: none; }
.sheet.has-foot .sheet-body { padding-bottom: 4px; }
.sheet-foot-in .acts { margin-top: 0; }
.sheet-foot-in .runstat { margin-top: 8px; text-align: center; }
.sheet-foot-in .runstat:first-child { margin-top: 0; margin-bottom: 8px; }

/* ---- search: CC's sticky pill ---- */
.searchwrap {
  position: sticky; top: 0; z-index: 5;
  background: var(--bg);
}
.sheet-body .searchwrap { background: var(--surf-1); }
.searchwrap {
  padding: 2px 0 10px;
  margin: -2px 0 0;
}
.search {
  display: flex; align-items: center; gap: 6px;
  background: var(--surf-2); border-radius: var(--r-pill);
  padding: 0 6px 0 14px; height: 46px;
  transition: background var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.search:focus-within { background: var(--surf-3); box-shadow: inset 0 0 0 1px var(--pri-line); }
.search svg { width: 20px; height: 20px; fill: var(--txt-faint); flex: none; }
.search input {
  flex: 1; min-width: 0;
  border: 0; outline: 0; background: transparent; color: var(--txt);
  font: 400 16px/1 Roboto, "Segoe UI", system-ui, sans-serif;
}
.search input::placeholder { color: var(--txt-faint); }
.search input::-webkit-search-cancel-button,
.search input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; display: none; }
/* `sclear`, deliberately not `clear`: a name no panel or card shares, so the X
   can never inherit someone else's padding and fall out of the 46px bar. */
.search .sclear {
  flex: none; width: 36px; height: 36px;
  display: grid; place-items: center;
  border: 0; border-radius: var(--r-pill);
  background: transparent; color: var(--txt-dim); cursor: pointer;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.search .sclear[hidden] { display: none; }   /* load-bearing: display:grid above beats [hidden] */
.search .sclear svg { width: 18px; height: 18px; fill: currentColor; display: block; }
.search .sclear:active { background: var(--press); color: var(--txt); }

/* ---- sign-in: a full screen, not a dialog. The token is the root key. ---- */
.login {
  position: fixed; inset: 0; z-index: 80;
  background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.login[hidden] { display: none; }
.login .login-form { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 6px; }
.login .login-fields { display: flex; flex-direction: column; gap: 6px; }
.login .logo { width: 44px; height: 44px; margin: 0 auto 10px; transform: none; }
.login h1 { font-size: 22px; font-weight: 600; margin: 0; text-align: center; letter-spacing: -.01em; }
.login .note { text-align: center; margin: 0 0 14px; }
.login .btn { margin-top: 8px; }

/* ---------------------------------------------------------------------------
 * PULL TO REFRESH - the puck. Ported from droplet-dock-app/web/app.css
 * (_Library/patterns/pull-refresh), tokens swapped for this app's. Only the puck
 * is styled - the JS owns its transform, and the content never moves.
 * ------------------------------------------------------------------------- */
.ptr {
  position: fixed;
  /* Just under the bar + status line, in the content: a pull can only start at
     the top of the page (scrollY 0) where they are on screen. env() with a
     fallback, because calc() with an unresolved var dies on a desktop browser
     that has no safe-area. */
  top: calc(env(safe-area-inset-top, 0px) + 72px);
  /* ⛔ NOT translateX(-50%) - the JS overwrites transform on every frame. */
  left: 50%;
  margin-left: -20px;
  width: 40px; height: 40px;
  box-sizing: border-box;
  border-radius: 50%;
  /* Above the page, below anything modal: a puck drawn over an open sheet reads
     as a stray element from another screen. */
  z-index: 40;
  background: var(--surf-3);
  box-shadow: 0 2px 8px rgba(0, 0, 0, .45);
  /* ⛔ NEVER HIT-TESTABLE. It sits at opacity 0 over the top of the page the
     whole time it is not in use, and a fixed element at opacity 0 still swallows
     every tap underneath it. Symptom: the first card "sometimes" ignores taps,
     only near the top. */
  pointer-events: none;
  /* Must agree with the JS: set(0) writes translateY(-24px) scale(.6). */
  opacity: 0;
  transform: translateY(-24px) scale(.6);
  will-change: transform, opacity;
  display: grid;
  place-items: center;
}
.ptr svg { width: 22px; height: 22px; fill: var(--pri); }
/* The transition is OPT-IN: while the finger is down there must be NO transition
   or the puck lags a full duration behind and the gesture feels simulated. The
   JS removes .settle on touchstart and adds it back on touchend, so the return
   journey animates and the outward one does not. Same idea as .dragging in the
   sheet, inverted. */
.ptr.settle { transition: transform .28s var(--ease), opacity .16s var(--ease); }
/* THE PUCK SETTLES AND HOLDS; ONLY THE ARROW SPINS. Rotating the whole element
   with a keyframe had to restate translateY at both ends or the puck slid back
   under the bar on every loop - spinning the child means the parent never moves
   once parked. */
.ptr.busy { transform: translateY(16px) scale(1); opacity: 1; }
.ptr.busy svg { animation: sp 720ms linear infinite; }
@media (prefers-reduced-motion: reduce) { .ptr.busy svg { animation-duration: 1.4s; } }

/* ---- toast, above the nav ---- */
.toast {
  position: fixed; left: 16px; right: 16px; z-index: 70;
  bottom: calc(var(--nav-h) + var(--safe-b) + 16px);
  padding: 12px 16px; border-radius: var(--r-sm);
  background: var(--surf-4); color: var(--txt); font-size: 14px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
  opacity: 0; transform: translateY(8px);
  transition: opacity var(--med) var(--ease), transform var(--med) var(--ease);
  pointer-events: none;
}
.toast.is-on { opacity: 1; transform: none; }

/* ---- desktop: the same phone layout, centred in one column. Not a second
   layout - the app is one thing on both screens. ---- */
@media (min-width: 820px) {
  header.bar, main { max-width: var(--col); margin-left: auto; margin-right: auto; }
  .navbar { justify-content: center; }
  .navitem { flex: 0 0 132px; }
  .fab { right: calc(50% - var(--col) / 2 + 16px); }
  .sheet { max-width: var(--col); margin: 0 auto; border-left: 1px solid var(--line); border-right: 1px solid var(--line); }
  .toast { max-width: 520px; margin: 0 auto; }
  .xrow.wide { display: flex; }
}

@media (prefers-reduced-motion: reduce) {
  .scrim, .sheet, .navitem .ind, .fab, .toast { transition-duration: .001ms; }
}
