﻿/*
  The panel — the product surface (spec §3, §4, §5). Dark-first; every colour
  comes from the shared brand layer, never a local literal.

  Motion rules (spec §10.2.2, §15 as amended by R9): animations are
  composite-only — transform and opacity — with the single documented exception
  of the card's own height collapse/expand, which is measured and run per-card
  via WAAPI (spec §10.2.2 allows a measured height on a clipped element).

  * Curves are NAMED, in brand.css: --ease-ui-out for everything, --ease-drawer
    for a sub-page arriving. Never a local cubic-bezier literal.
  * Every `transition` names exact properties. Never `all` — it animates things
    you didn't mean to and pays layout for them.
  * Exits are always faster than entries. Leaving should feel like getting out
    of the way; arriving can take its time.
  * Ease-out with no overshoot: inza doesn't bounce, it arrives.

  prefers-reduced-motion is deliberately ignored (AGENTS.md §2 rule 9).
*/

@import url("../brand.css");

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
  background: transparent;
  font-family: var(--font-ui);
  color: var(--color-text);
  user-select: none;
  overflow: hidden;
}

/* ── Scrollbars ─────────────────────────────────────────────────────────────
   Every scroller in the panel, not just the card list: the settings page, the
   archive, the note input and a card's edit textarea all scroll, and the OS
   scrollbar Windows draws in them is a grey slab with none of this window's
   colour in it. One rule, on every element, so a new scroller can never arrive
   with the default.

   Both themes come free — the thumb is a border token, which is already the
   right value in each. The track is transparent because the panel's own surface
   IS the track. */
*::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

*::-webkit-scrollbar-track,
*::-webkit-scrollbar-corner {
  background: transparent;
}

*::-webkit-scrollbar-thumb {
  border-radius: 3px;
  background: var(--color-border);
}

/* The muted token rather than the strong border: in the light theme
   --color-border-strong lands within a couple of values of --color-border, so
   the hover would be invisible in exactly one of the two themes. This one reads
   as a step up in both. (No transition — scrollbar parts don't take one.) */
*::-webkit-scrollbar-thumb:hover {
  background: var(--color-muted);
}

/* ── Window state: expanded | rail (spec §3.3) ──────────────────────────────
   Main owns the state machine; the body attribute is the only thing that
   decides which surface is mounted. Both start hidden so a cold start can't
   flash the wrong one before main answers with the persisted mode. */

#panel,
#rail {
  display: none;
}

/* The rail's swap motion (rev-6.10): a fast fade both ways - entering after a
   collapse it ignites at the spot the condensed panel converged on (no pose,
   no second arrival), and leaving for an expand it swells toward the panel it
   is becoming (the transform below) while it dissolves. The opacity length is
   matched by panel-window.ts's EXPAND_OUT_MS - don't retune one without the
   other. */
#rail {
  transform-origin: right center;
  transition: opacity 70ms var(--ease-ui-out),
              transform 90ms var(--ease-ui-out);
}
body[data-panel-edge="left"] #rail { transform-origin: left center; }

body[data-panel-mode="expanded"] #panel { display: flex; }
body[data-panel-mode="rail"] #rail { display: flex; }

/* Held across the swap: the outgoing surface leaves under this class, the
   window resizes while nothing is visible, and the incoming one settles in
   when it lifts. Each direction is ONE movement with the capsule as its pivot
   (rev-6.10) — never "a surface vanishes, another appears". */
/* EXPANDING (rev-6.6): the panel is already essentially in place - 96.5%
   scale with 8px of edge-bias, so it reads as surfacing from the rail rather
   than travelling in from somewhere. It comes into view; it does not arrive. */
body.is-swapping[data-swap-to="expanded"] #panel { opacity: 0; transform: translateX(8px) scale(0.965); }
body[data-panel-edge="left"].is-swapping[data-swap-to="expanded"] #panel { transform: translateX(-8px) scale(0.965); }

/* COLLAPSING: the panel CONDENSES INTO THE CAPSULE'S SPOT - transform-origin
   is the docked edge's center (set on #panel above), which is exactly where
   the pill lives, so the panel's last visible form is a pill-sized sliver at
   the pill's own position. Scale, not width/height: at a 0.07 sliver the
   corner distortion is invisible and the morph stays composite-only.
   Opacity ends 25ms before main's resize deadline (COLLAPSE_OUT_MS): the
   renderer starts this an IPC hop after main starts its timer, and equal
   lengths meant a late morph was sometimes caught half-visible by the resize
   - the old "sometimes it flashes" inconsistency. */
body.is-swapping[data-swap-to="rail"] #panel {
  opacity: 0;
  transform: scale(0.07, 0.16);
  transition: opacity 175ms var(--ease-ui-out), transform 200ms var(--ease-drawer);
}
/* The pill needs NO entrance pose after a collapse: the condensed panel just
   BECAME it, at its exact spot - railBounds centers the rail window on the
   panel's own center-y, flush to the same edge. It only fades in, fast (the
   70ms opacity transition on #rail), so the handoff reads as the mark
   igniting where the eye is already looking. The old scaled ghost
   (scale(1.7, 1.15) on a spring) was a SECOND arrival - the very
   "vanish, then the pill resurfaces" read this revision removes. */
body.is-swapping[data-swap-to="rail"] #rail { opacity: 0; }
/* Leaving for an expand, the capsule swells toward the panel it is becoming
   as it dissolves - the click's acknowledgement is instant, and the panel's
   entrance picks the growth up from the same edge. */
body.is-swapping[data-swap-to="expanded"] #rail {
  opacity: 0;
  transform: scale(1.35, 1.06);
}

#panel {
  position: relative;
  flex-direction: column;
  height: 100%;
  /* The glass shell (rev-6): one translucent surface, lit from above — the rim
     is an inset highlight, never a brighter border, and the shadow is a warm
     layered ladder rather than one black blob. */
  background: var(--glass-panel);
  border: 1px solid var(--glass-border);
  border-radius: 16px;
  corner-shape: squircle;
  box-shadow: inset 0 1px 0 var(--glass-rim), var(--shadow-glass);
  overflow: hidden;
  transform-origin: right center;
  transition: opacity 100ms var(--ease-ui-out), transform 200ms var(--spring-panel);
}
body[data-panel-edge="left"] #panel { transform-origin: left center; }

/* ── Resize grips (spec §3.1, R1.2) ─────────────────────────────────────────
   Native resize is not an option here (a transparent window doesn't resize
   from its frame), so both axes are driven by these grips through main: the
   side grip owns width, the bottom grip owns height. Identical in feel — same
   6 px target, same hairline that only appears under the pointer. */

#resize-grip,
#resize-grip-bottom {
  position: absolute;
  z-index: 4;
  -webkit-app-region: no-drag;
}

#resize-grip {
  top: 0;
  bottom: 0;
  width: 6px;
  cursor: ew-resize;
}

#resize-grip-bottom {
  left: 0;
  right: 0;
  bottom: 0;
  height: 6px;
  cursor: ns-resize;
}

body[data-panel-edge="right"] #resize-grip { left: 0; }
body[data-panel-edge="left"] #resize-grip { right: 0; }

/* A hairline that only shows while the grip is live — the affordance appears
   under the pointer, it doesn't decorate the panel. */
#resize-grip::after,
#resize-grip-bottom::after {
  content: "";
  position: absolute;
  border-radius: 1px;
  background: var(--color-border-strong);
  opacity: 0;
  transition: opacity 120ms var(--ease-ui-out);
}

#resize-grip::after {
  top: 0;
  bottom: 0;
  left: 2px;
  width: 2px;
}

#resize-grip-bottom::after {
  left: 0;
  right: 0;
  bottom: 2px;
  height: 2px;
}

#resize-grip:hover::after,
#resize-grip-bottom:hover::after,
body.is-resizing #resize-grip::after,
body.is-resizing #resize-grip-bottom::after {
  opacity: 1;
}

/* ── Rail (spec §3.3.2, rev-6.3) ──────────────────────────────────────────
   The rail window is a fixed 22×96 rectangle hugging the capsule (see
   panel-geometry.ts). The button fills it — a transparent window still eats
   clicks, so every one of its pixels should at least DO the obvious thing —
   but the pointer cursor, the morph, and the chip all key on the capsule's own
   hover (its ::before halo), never on the window's dead fringe. */

#rail {
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: transparent;
  /* The whole strip is an honest target again (rev-6.7): it is tall so a
     thrown cursor lands in it without fine vertical aim, the capsule GLIDES to
     meet the pointer (panel-window-state.ts), and a click anywhere opens the
     panel — so the pointer cursor tells the truth everywhere in it. */
  cursor: pointer;
}

/* The lit keyline (rev-6.9, the pill-lab pick, P7): a 10×58 capsule whose
   armor is the DUAL KEYLINE — a light inner ring and a dark outer ring, so one
   of them always contrasts with whatever the pill floats over (the mouse
   cursor's trick; the rev-4 pill knew it, rev-6 lost it) — and whose heart is
   the FILAMENT: a self-luminous accent line no ground can hide (teal since
   rev-6.16, when the oxblood retired with inject). States read as energy: awake widens and brightens the filament, hover
   stretches it as the capsule takes the first frame of the expansion. */
.rail-pill {
  position: absolute;
  top: 50%;
  width: 10px;
  height: 58px;
  margin-top: -29px;
  background: linear-gradient(90deg, rgba(30, 26, 22, 0.88) 0%, rgba(22, 19, 16, 0.82) 100%);
  border: 1px solid rgba(255, 255, 255, 0.58);
  box-shadow: -1px 0 0 rgba(0, 0, 0, 0.4),
              -2px 2px 10px rgba(0, 0, 0, 0.35),
              inset 0 1px 0 rgba(255, 255, 255, 0.28);
  transition: width 240ms var(--ease-ui-out), height 240ms var(--ease-ui-out),
              margin-top 240ms var(--ease-ui-out),
              transform 170ms var(--ease-ui-out),
              background-color 220ms ease, border-color 220ms ease,
              box-shadow 220ms ease, filter 140ms ease;
}
body[data-panel-edge="right"] .rail-pill { right: 0; border-right: 0; border-radius: 10px 0 0 10px; }
body[data-panel-edge="left"] .rail-pill {
  left: 0;
  border-left: 0;
  border-radius: 0 10px 10px 0;
  background: linear-gradient(270deg, rgba(30, 26, 22, 0.88) 0%, rgba(22, 19, 16, 0.82) 100%);
  box-shadow: 1px 0 0 rgba(0, 0, 0, 0.4),
              2px 2px 10px rgba(0, 0, 0, 0.35),
              inset 0 1px 0 rgba(255, 255, 255, 0.28);
}

/* The capsule's own hover zone: this pseudo extends the hitbox a comfortable
   halo past its ends and inward, and the morph keys on the PILL's hover, never
   the window's (rev-6.2). */
.rail-pill::before {
  content: "";
  position: absolute;
  top: -16px;
  bottom: -16px;
  left: -10px;
  right: -10px;
}

/* The accent filament — alive at rest, silent at rest. Brightest at its center,
   fading toward its ends; it gets LONGER and WARMER with state, which reads as
   energy where a swelling dot only reads as inflation. */
.rail-pill::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 2.5px;
  height: 22px;
  margin: -11px 0 0 -1.25px;
  border-radius: 3px;
  background: linear-gradient(180deg,
    rgba(126, 165, 170, 0.3) 0%, var(--color-accent) 30%, #8FBCC1 50%,
    var(--color-accent) 70%, rgba(126, 165, 170, 0.3) 100%);
  box-shadow: 0 0 6px rgba(94, 138, 143, 0.55);
  transition: height 240ms var(--ease-ui-out), margin-top 240ms var(--ease-ui-out),
              box-shadow 220ms ease;
}

/* Awake (rev-6.8): the pointer entered the proximity circle — main saw it
   coming (rail-approach.ts). The capsule stirs before it is touched. */
body.is-rail-near .rail-pill { width: 13px; }
body.is-rail-near .rail-pill::after {
  height: 28px;
  margin-top: -14px;
  box-shadow: 0 0 9px rgba(94, 138, 143, 0.7);
}

/* Hover: the first frame of the expansion, sampled. */
.rail-pill:hover {
  width: 17px;
  height: 66px;
  margin-top: -33px;
}
.rail-pill:hover::after {
  height: 30px;
  margin-top: -15px;
  box-shadow: 0 0 9px rgba(94, 138, 143, 0.7);
}
#rail:active .rail-pill { filter: brightness(1.12); }

/* A capture landed while the rail is up (rev-6.11): the capsule pulses — the
   filament flares long and bright, the sheath lifts — and settles back to
   whatever state the pill is in (no 100% frame, so awake/rest both return
   true). This is the presence acknowledging a hold; the capture toast is gone. */
body.is-rail-held .rail-pill {
  animation: pill-held 900ms var(--ease-ui-out);
}
body.is-rail-held .rail-pill::after {
  animation: pill-held-filament 900ms var(--ease-ui-out);
}
@keyframes pill-held {
  20% { filter: brightness(1.4); }
}
@keyframes pill-held-filament {
  20% {
    height: 36px;
    margin-top: -18px;
    box-shadow: 0 0 14px rgba(94, 138, 143, 0.95);
  }
}



/* ── Header (spec §4.1) ─────────────────────────────────────────────────── */

#panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: 0 0 auto;
  padding: 12px 14px 10px;
  /* The header is the drag region (spec §3.4.1); buttons opt back out. */
  -webkit-app-region: drag;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

/* Every glyph in `ınza·` sits at the x-height — the ı is dotless and nothing
   ascends or descends — so the mark reads far smaller than its font-size and at
   13 px looked clipped rather than small. 16 px buys back the x-height. The
   line-height is explicit (not `normal`) so the box is a known 19 px: shorter
   than the 22 px header buttons, which keeps the header exactly as tall as it
   was, and taller than the glyphs, so nothing can crop at any panel width. */
.wordmark {
  font-family: var(--font-display);
  font-size: 17px;
  line-height: 1.2;
  font-weight: var(--weight-display);
  letter-spacing: 0.01em;
  color: var(--color-text);
}

.wordmark-dot {
  color: var(--color-accent);
}

/* A sub-page names itself where the wordmark would be (spec §4.2, R4.2). */
.page-title {
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.01em;
  color: var(--color-text);
}

/* Which header controls belong to which surface (R4.2, R4.3, R4.4).
   Stream: camera · search · ⋯ · collapse, with the wordmark.
   Archive: back · name · search — the ⋯ menu is stream-only, and archive's one
   entry point is the archive row (one path per destination).
   Settings: back · name, and nothing to do but leave. */
body[data-view-mode="stream"] #back-btn,
body:not([data-view-mode="stream"]) .wordmark,
body:not([data-view-mode="stream"]) #snip-btn,
body:not([data-view-mode="stream"]) #overflow-btn,
body:not([data-view-mode="stream"]) #collapse-btn,
body[data-view-mode="settings"] #search-btn {
  display: none;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 3px;
}

/* Splits the content tool (search) from the app chrome (gear, collapse) —
   the header's groups are legible without reading the icons (rev-6.1). */
.header-sep {
  width: 1px;
  height: 15px;
  margin: 0 5px;
  background: var(--glass-hairline);
}

.header-btn {
  -webkit-app-region: no-drag;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
  transition: color 120ms var(--ease-ui-out), background-color 120ms var(--ease-ui-out),
              transform var(--motion-press);
}

.header-btn:hover,
.header-btn.is-on {
  color: var(--color-text);
  background: var(--color-subtle-fill);
}

/* ── Pressables (R9.2) ──────────────────────────────────────────────────────
   Anything you press acknowledges the press itself: a 0.97 scale on :active,
   transform-only, 140 ms. One rule so every control in the panel answers the
   same way — and deliberately NOT on the check circle, whose own fill-pulse
   (R9.3) owns its transform, nor the rail, whose press has to compose with the
   hover morph and so lives with the rest of the rail above. */

.header-btn:active:not(:disabled),
.card-action:active,
.archive-row:active,
.shortcut-chip:active,
.settings-btn:active,
.settings-seg-btn:active {
  transform: scale(0.97);
}

/* ── Search bar (spec §4.5.1) ───────────────────────────────────────────────
   Slides down from under the header in 160 ms — the FIELD slides, on a clipped
   wrapper whose height is snapped rather than animated (spec §10.2.2). Animating
   the wrapper's height instead, as this once did, relaid out the whole card
   scroller on every one of those frames.

   So the slot opens in a single layout pass and the field translates down into
   it. Closing needs the reverse, which is what the 0s-but-delayed height
   transition buys: the slot stays open for the length of the slide, then snaps
   shut once the field has gone.

   The rules on the BASE selectors are the closing ones and the rules under
   `.is-searching` are the opening ones, which is how the exit gets to be faster
   than the entry (R9.1) — 130 ms out, 160 ms in. The slot's delay must equal the
   field's outgoing duration, or it snaps shut over a field still sliding. */

.search-bar {
  /* Declared here, inherited by the field below — the slot's height and the
     distance the field is parked above it are the same number by definition. */
  --search-bar-h: 36px;
  --search-out: 130ms;
  flex: 0 0 auto;
  height: 0;
  overflow: hidden;
  padding: 0 12px;
  transition: height 0s linear var(--search-out);
}

body.is-searching .search-bar {
  height: var(--search-bar-h);
  transition: height 0s linear 0s;
}

.search-input {
  width: 100%;
  padding: 6px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-control-fill);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  user-select: text;
  outline: none;
  opacity: 0;
  /* Parked a full slot above the clip, so none of it shows through. */
  transform: translateY(calc(var(--search-bar-h) * -1));
  transition: transform var(--search-out) var(--ease-ui-out),
              opacity var(--search-out) var(--ease-ui-out),
              border-color 120ms var(--ease-ui-out);
}

body.is-searching .search-input {
  opacity: 1;
  transform: translateY(0);
  transition: transform 160ms var(--ease-ui-out),
              opacity 160ms var(--ease-ui-out),
              border-color 120ms var(--ease-ui-out);
}

.search-input:focus { border-color: var(--color-accent-border); }
.search-input::placeholder { color: var(--color-muted); }

/* ── One surface, temporary pages (spec §4, R4.1) ───────────────────────────
   The stream and its footer stay mounted; archive and settings translate in
   from the right over the top of them. Not three sibling modes — one place
   you are, and pages you visit — so coming back is instant and stateless.

   Transform only (spec §10.2.2). The page carries its own background because
   it is genuinely on top of something. */

#panel-body {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

#view-stream {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.subpage {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--color-panel);
  transform: translateX(100%);
  /* #panel's overflow:hidden clips a parked page visually, but clipping is not
     hit-testing — without this it would still swallow clicks meant for the
     stream underneath. */
  pointer-events: none;
  /* Exit: faster than the entry (R9.1), on the UI ease-out. */
  transition: transform 200ms var(--ease-ui-out);
}

.subpage.is-open {
  transform: translateX(0);
  pointer-events: auto;
  /* Entry: the drawer curve (R4.1) — a long, soft settle that reads as a
     surface arriving rather than a view being swapped. */
  transition: transform 240ms var(--ease-drawer);
}

/* ── Card list ──────────────────────────────────────────────────────────── */

.card-list {
  position: relative; /* the drop indicator anchors to the list, not the page */
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 2px 12px 10px;
  /* Cards space themselves with a margin rather than a flex gap: a gap can't be
     animated per card, and the check-off collapse has to take its own spacing
     with it (spec §8.1.2). */
}

/* Rows, not boxed cards (rev-6, the composite): each hold is etched into the
   glass with a hairline below it, no per-card frame — the strokes the old
   boxes spent are gone. Resting rows sit at 78% ink; a TICKED row comes to
   full ink with its gutter dot lit. Recognition by silhouette, weight by
   staging state. */
.card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-bottom: 0;
  padding: 10px 12px;
  border: 0;
  border-bottom: 1px solid var(--glass-hairline);
  border-radius: 0;
  background: transparent;
  cursor: default;
  transition: background-color 120ms var(--ease-ui-out);
}
.card:last-child { border-bottom: 0; }

.card:hover { background: var(--glass-hover); }

.card-main { transition: opacity 140ms ease; opacity: 0.78; }
.card:hover .card-main { opacity: 0.9; }
.card.is-checked .card-main { opacity: 1; }

/* Ticked (the composite grammar): the gutter dot lights up in the accent and the row
   comes to full ink — see .card-check below. No fills, no borders: the
   material never changes, only the light on it. */

/* The keyboard focus ring (R2.6) — independent of ticked, and drawn as an
   outline so a card can show both at once without the two fighting. */
.card.is-focused {
  outline: 1px solid var(--color-border-strong);
  outline-offset: 1px;
}

.card-main {
  flex: 1 1 auto;
  min-width: 0;
}

/* The check circle (spec §5.3, R2.1). An open circle reads as a checkbox to
   everyone, so it IS one: click it (or the card) to put the card in the next
   inject. 12 px rather than the original 10 — it has a tick to hold now, and it
   is a primary target rather than decoration. */
.card-check {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 13px;
  height: 13px;
  margin-top: 3px;
  padding: 0;
  border: 1.5px solid var(--glass-ring);
  border-radius: 50%;
  background: transparent;
  /* The tick is present but invisible until checked, so the circle never
     reflows when it fills. */
  color: transparent;
  cursor: pointer;
  transition: border-color 120ms var(--ease-ui-out), background-color 120ms var(--ease-ui-out), color 120ms var(--ease-ui-out), box-shadow 120ms var(--ease-ui-out);
}

.card-check:hover {
  border-color: var(--color-accent);
}

/* Lit: the accent dot with a soft glow — "in the next copy" as light, the
   same grammar the Inject button's count answers. */
.card.is-checked .card-check {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-text-on-accent);
  box-shadow: 0 0 8px rgba(94, 138, 143, 0.5);
}

/* Ticking is the highest-frequency action in the product, so it gets the
   smallest possible celebration (R9.3): the fill is instant, and this is a
   ~120 ms scale pulse on the circle alone. Replayed by removing/re-adding the
   class, the same way the archive row's flash is. */
.card-check.is-pulsing {
  animation: check-pulse 120ms var(--ease-ui-out);
}

@keyframes check-pulse {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.25); }
  100% { transform: scale(1); }
}

/* The hover action: archive in the stream, restore in the archive (R3.1, R3.3).
   ONE slot for every kind — absolutely anchored top-right beside the meta chip
   — so the gesture lives in the same place on a text row and an image tile
   (it used to wrap to the bottom-left on thumb cards, which made the archive
   button roam by kind). Revealed on hover rather than always shown — the card
   is a thing you read, not a row of buttons. */
.card-action {
  position: absolute;
  top: 9px;
  right: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  /* The meta chip's own backing, on EVERY kind — over an image it kept the
     glyph legible, and on text rows the bare glyph was the one hover overlay
     with nothing under it. The two chips now read as a matched pair. */
  border-radius: var(--radius-sm);
  border: 1px solid var(--glass-border);
  background: var(--color-window-fill-strong);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  color: var(--color-muted);
  cursor: pointer;
  opacity: 0;
  transition: opacity 120ms var(--ease-ui-out), color 120ms var(--ease-ui-out), background-color 120ms var(--ease-ui-out);
}

/* Keyboard reachability doesn't depend on a pointer: the ring shows it too. */
.card:hover .card-action,
.card.is-focused .card-action,
.card-action:focus-visible {
  opacity: 1;
}

.card-action:hover {
  color: var(--color-text);
  background: var(--color-control-hover);
}

.card-title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: 1.35;
  color: var(--color-text);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

/* Body preview: clamped to two lines (spec §5.3) — the card is a reminder of
   what you saved, not a reader. */
.card-body {
  margin-top: 2px;
  font-size: 13.5px;
  line-height: 1.4;
  color: var(--color-muted-strong);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

/* Captured text is a quotation (rev-6, the card language): a selection's body
   renders in the brand serif at reading size. The serif belongs to captured
   material only — chrome stays in the sans, and the two never mix. */
.card[data-kind="selection"] .card-body {
  font-family: var(--font-display);
  font-size: 14.5px;
  line-height: 1.45;
  color: var(--color-text);
}

/* Provenance is precious on demand and noise always-on (rev-6): the meta line
   is a chip that appears top-right on hover, over the row rather than under
   it — the resting row spends its height on the held thing alone. */
.card-meta {
  position: absolute;
  top: 9px;
  right: 34px;
  max-width: 55%;
  height: 20px;
  display: inline-flex;
  align-items: center;
  padding: 0 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--glass-border);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  background: var(--color-window-fill-strong);
  font-family: var(--font-ui);
  font-size: 11px;
  color: var(--color-muted-strong);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms ease;
}
.card:hover .card-meta,
.card.is-focused .card-meta { opacity: 1; }

/* Pending url fetch: the fetch state is news, not chrome — it stays visible
   without a pointer, pulsing until the text lands (spec §5.3). */
.card.is-pending .card-meta {
  opacity: 1;
  animation: meta-pulse 1.4s ease-in-out infinite;
}

@keyframes meta-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.6; }
}

/* Snips and image files are recognized by PICTURE, not words (rev-6, the
   mosaic's one steal): the thumbnail is a full-width tile beside the gutter
   dot, and the title/meta wrap beneath it. Served over the inza-image://
   protocol — never as base64 over IPC (spec §10.2.5). */
.card:has(.card-thumb) { flex-wrap: wrap; }
.card-thumb {
  flex: 1 1 0;
  min-width: 0;
  height: 64px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-surface);
}
.card:has(.card-thumb) .card-main {
  flex: 1 1 100%;
  margin-top: 6px;
}

.card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}


/* File-type chip (spec §5.3) — what the card IS, in one glance. */
.card-chip {
  display: inline-block;
  margin-right: 6px;
  padding: 1px 5px;
  border-radius: var(--radius-xs);
  background: var(--color-subtle-fill);
  color: var(--color-muted);
  font-family: var(--font-ui);
  font-size: 10px;
  letter-spacing: 0.01em;
  vertical-align: 1px;
}

/* Inline editing (spec §6.2): the card becomes a plain textarea in place. */
.card.is-editing {
  border-color: var(--color-accent-border);
  background: var(--color-panel-raised);
}

.card-editor {
  width: 100%;
  max-height: 40vh;
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: 12.5px;
  line-height: 1.45;
  resize: none;
  outline: none;
  overflow-y: auto;
  user-select: text;
}

/* Drag reorder (spec §6.1.4): the lifted card follows the pointer on a
   transform, and the gap it will land in is marked by a hairline. */
.card.is-dragging {
  z-index: 3;
  opacity: 0.92;
  box-shadow: var(--shadow-float);
  cursor: grabbing;
}

.card.is-drag-ghost {
  opacity: 0.35;
}

/* Absolutely positioned so it cannot shift the rows it points between — see the
   measuring note in panel-drag.ts. */
.drop-indicator {
  position: absolute;
  left: 12px;
  right: 12px;
  height: 2px;
  border-radius: 1px;
  background: var(--color-accent);
  pointer-events: none;
}

/* ── Archive row + divider (spec §4.2.3, §4.5.2) ────────────────────────── */

.archive-row {
  flex: 0 0 auto;
  margin: 0 12px 8px;
  padding: 6px 2px 0;
  border: 0;
  border-top: 1px dashed var(--glass-hairline);
  background: transparent;
  color: var(--color-muted);
  font-family: var(--font-ui);
  font-size: 11.5px;
  letter-spacing: 0.01em;
  text-align: left;
  cursor: pointer;
  transition: color 120ms var(--ease-ui-out);
}

.archive-row:hover { color: var(--color-text); }

/* Flashed once when aging or a check-off raises the count (spec §8.2.4) —
   tidying, not activity. */
.archive-row.is-flashing {
  animation: archive-flash 900ms ease-out;
}

@keyframes archive-flash {
  0%   { color: var(--color-accent); transform: none; }
  30%  { color: var(--color-accent); transform: translateY(-1px); }
  100% { color: var(--color-muted); transform: none; }
}

.archive-divider {
  margin: 12px 2px 8px;
  padding-top: 6px;
  border-top: 1px dashed var(--glass-hairline);
  color: var(--color-muted);
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.01em;
  opacity: 0.75;
}

/* ── Empty state (spec §4.2) ────────────────────────────────────────────── */

.empty-state {
  margin: auto;
  padding: 24px 22px;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: 1.5;
  color: var(--color-muted);
  text-align: center;
}

.empty-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 1px 6px;
  margin: 0 1px;
  border: 1px solid var(--color-border-soft);
  border-radius: var(--radius-sm);
  background: var(--color-control-fill);
  color: var(--color-text-strong);
  font-family: var(--font-ui);
  font-size: 0.8em;
  line-height: 1.3;
  box-shadow: var(--shadow-keycap);
}

/* ── Settings view (spec §4.4, §13) ─────────────────────────────────────── */

.settings-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 4px 14px 16px;
}

.settings-block {
  padding-top: 8px;
}

.settings-heading {
  margin-bottom: 8px;
  font-family: var(--font-ui);
  font-size: 11.5px;
  font-weight: var(--weight-medium);
  letter-spacing: 0.04em;
  color: var(--color-muted);
}

.settings-wordmark {
  font-family: var(--font-display);
  font-size: 18px;
  color: var(--color-text);
}

.settings-version {
  margin-top: 2px;
  font-family: var(--font-ui);
  font-size: 11.5px;
  color: var(--color-muted);
}

/* The form (spec §13). Every row is label-left / control-right, stacking only
   where a control can't survive the 280 px minimum width (spec §3.1). */

/* Blocks are separated by a hairline. About sits outside #settings-form (it is
   the one block that is real in the HTML), so it needs the rule spelled out. */
.settings-block + .settings-block,
#settings-form + .settings-block {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--glass-hairline);
}

.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* A real gutter: a wrapping label ("Everything inza holds lives on this
     device.") used to run right up against its button before breaking. */
  gap: 18px;
  min-height: 30px;
  padding: 3px 0;
}

.settings-row.is-stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.settings-row-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.settings-row-label {
  font-size: var(--text-sm);
  color: var(--color-text);
  /* A label that has to wrap breaks into even lines ("Everything inza holds /
     lives on this device.") instead of filling the first line to the brim and
     stranding one word on the second. No-op on single-line labels. */
  text-wrap: balance;
}

.settings-hint {
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--color-muted);
}

.settings-row-control {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

.settings-status {
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  color: var(--color-accent);
}

.settings-status.is-error { color: var(--color-danger); }

/* This sets `display`, which beats the UA's `[hidden] { display: none }` — so
   hiding it from script has to be spelled out here or the attribute is
   silently inert. */
.shortcut-clear[hidden] { display: none; }

/* Shortcut chip: mono, keycap-like, matching the empty state's binding chip. */

.shortcut-chip {
  padding: 3px 8px;
  border: 1px solid var(--color-border-soft);
  border-radius: var(--radius-sm);
  background: var(--color-control-fill);
  color: var(--color-text-strong);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  line-height: 1.4;
  cursor: pointer;
  box-shadow: var(--shadow-keycap);
  transition: border-color var(--motion-decisive), color var(--motion-decisive);
}

.shortcut-chip:hover { border-color: var(--color-border-strong); }
.shortcut-chip.is-unset { color: var(--color-muted); }

.shortcut-chip.is-capturing {
  border-color: var(--color-accent-border);
  background: var(--color-accent-soft);
  color: var(--color-text);
}

.shortcut-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--color-muted);
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.shortcut-clear:hover {
  background: var(--color-subtle-hover);
  color: var(--color-text);
}

/* Toggle. The knob moves on transform only (spec §10.2.2). */

.settings-toggle {
  position: relative;
  flex: 0 0 auto;
  width: 30px;
  height: 17px;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  border-radius: 9px;
  background: var(--color-control-fill);
  cursor: pointer;
  transition: background var(--motion-decisive), border-color var(--motion-decisive);
}

.settings-toggle.is-on {
  border-color: var(--color-accent-border);
  background: var(--color-accent);
}

.settings-toggle-knob {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--color-muted-strong);
  transition: transform var(--motion-calm), background var(--motion-decisive);
}

.settings-toggle.is-on .settings-toggle-knob {
  background: var(--color-text-on-accent);
  transform: translateX(13px);
}

/* Segmented picker */

.settings-seg {
  display: flex;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--color-border-soft);
  border-radius: var(--radius-md);
  background: var(--color-control-fill);
}

.is-stacked .settings-seg { width: 100%; }

.settings-seg-btn {
  flex: 1 1 auto;
  padding: 4px 9px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--motion-decisive), background var(--motion-decisive);
}

.settings-seg-btn:hover { color: var(--color-text); }

.settings-seg-btn.is-active {
  background: var(--color-surface-hover);
  color: var(--color-text);
}

/* Number field + its unit */

.settings-number {
  width: 52px;
  padding: 4px 6px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-control-fill);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  text-align: right;
  user-select: text;
  outline: none;
  transition: border-color var(--motion-decisive);
}

.settings-number:focus { border-color: var(--color-accent-border); }

.settings-unit {
  font-size: var(--text-xs);
  color: var(--color-muted);
}

/* Plain action button (Open data folder) */

.settings-btn {
  flex: none;
  padding: 6px 11px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-control-fill);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--motion-decisive);
}

.settings-btn:hover { background: var(--color-control-hover); }

/* Unlock block: the license key field (settings-license.ts) */

.settings-key-row {
  display: flex;
  gap: 6px;
}

.settings-key-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-control-fill);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: var(--text-xs);
  user-select: text;
  outline: none;
  transition: border-color var(--motion-decisive);
}

.settings-key-input:focus { border-color: var(--color-accent-border); }
.settings-key-input::placeholder { color: var(--color-muted); }

/* ── Footer: note input + inject (spec §4.2.4-4.2.5) ────────────────────── */

/* Stream-only by construction, not by a display rule: the footer belongs to the
   stream surface, and a sub-page covers it along with the list (R4.1). Hiding it
   instead would relayout the stream underneath mid-slide. */
#panel-footer {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px 12px;
  border-top: 1px solid var(--glass-hairline);
}

/* The field wraps the input so the capture chip can sit inside the input's
   visual bounds (spec §4.2.4). */
.note-field {
  position: relative;
}

.note-input {
  /* A textarea is inline-block by default, which would leave a baseline gap
     under it inside the wrapper. */
  display: block;
  width: 100%;
  /* Grows to at most 6 lines, then scrolls internally (spec §4.2.4). The height
     itself is set by the renderer from the measured content. */
  max-height: 156px;
  /* The resting height is a line CENTERED in a roomier box (the padding does
     the centering), so the composer reads as a place to write rather than a
     slot to fill — and never renders shorter than its own placeholder. */
  min-height: 44px;
  padding: 12px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-control-fill);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  line-height: 1.4;
  resize: none;
  overflow-y: auto;
  user-select: text;
  outline: none;
  transition: border-color 120ms var(--ease-ui-out);
}

.note-input::placeholder { color: var(--color-muted); }
.note-input:focus { border-color: var(--color-accent-border); }

/* No scrollbar inside the composer, ever: the global 6px thumb flashing beside
   a one-line field during the auto-size dance read as phantom scrolling. Past
   the 6-line cap the text still scrolls with the caret and wheel; the moving
   text is its own affordance. */
.note-input::-webkit-scrollbar { display: none; }


/* The Counter footer (rev-3): the verbs are content-sized beside the input, not
   a banner. The row bottom-aligns so a growing textarea rises alone. */
.footer-row {
  display: flex;
  gap: 8px;
  align-items: flex-end;
}
.footer-row .note-field { flex: 1; min-width: 0; }

/* The camera inside the note field (rev-6.2): both are ways to MAKE a hold, so
   they share one control — and the input keeps the width its placeholder needs
   (outside the field it squeezed "Hold a thought…" into a wrap). Anchored to
   the bottom so it stays put while the textarea grows upward; 9px centers the
   26px button in the 44px resting box. */
.note-snip {
  position: absolute;
  left: 4px;
  bottom: 9px;
  z-index: 1;
}
.note-field .note-input { padding-left: 33px; }

/* Empty, the textarea holds only its placeholder: keep that to ONE line however
   narrow the field gets (a wrapped, clipped placeholder was the recurring bug).
   panel-note.ts toggles the class as the value changes; typed text wraps as
   normal the moment the field is non-empty. */
.note-input.is-empty { white-space: nowrap; }

/* Copy — the ONE delivery verb (rev-6.14): it wears the accent, because handing
   the payload over IS the product's decisive moment now. Its height matches
   the note input's RESTING height (44px, .note-input) so the two read as one
   bar; the row bottom-aligns, so a growing textarea rises alone. */
.copy-btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 44px;
  padding: 0 15px;
  border: 0;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-text-on-accent);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  /* The copy moment is the one fast, decisive motion (spec §15.2); the press
     itself answers on the shared pressable timing (R9.2).  */
  transition: opacity var(--motion-decisive), transform var(--motion-press);
}

.copy-btn:hover:not(:disabled) { opacity: 0.92; }
.copy-btn:active:not(:disabled) { transform: scale(0.97); }

/* The slot exists so the button's tooltip survives being disabled: a disabled
   control is not a pointer target, so the tip hangs off the wrapper and the
   button stops swallowing the hit. The cursor moves here with it. */
.copy-slot { cursor: default; }

.copy-btn:disabled {
  opacity: 0.4;
  pointer-events: none;
}

/* ── Tooltips (spec §4.1, §12.1, §15.9) ─────────────────────────────────────
   ONE element, re-labelled and moved per trigger by panel-tooltip.ts. It sits on
   <body> rather than inside #panel, which clips its overflow and wears a
   transform mid-swap — either would crop a fixed-position child.

   Entry 140 ms, exit 100 ms: leaving is faster than arriving (R9.1). Transform
   and opacity only, exact properties named. `transform-origin` is set per
   placement so the tip grows out of the control it explains.

   `.is-instant` kills the ENTRY only — it is scoped to `.is-open`, so removing
   that class stops matching and the base exit still eases. That is the skip
   delay's second half: a tip that follows another appears with no animation at
   all, but never vanishes abruptly. */

.panel-tip {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 20;
  /* The panel is 280-420 px wide; the viewport clamp is what keeps a tip inside
     it at the minimum. */
  max-width: min(220px, calc(100vw - 16px));
  padding: 4px 7px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-window-fill-strong);
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: 10.5px;
  line-height: 1.35;
  box-shadow: var(--shadow-float);
  /* Never a hover target itself, or it would chase the pointer off its trigger. */
  pointer-events: none;
  opacity: 0;
  transform: scale(0.97);
  transition: opacity 100ms var(--ease-ui-out), transform 100ms var(--ease-ui-out);
}

.panel-tip.is-open {
  opacity: 1;
  transform: scale(1);
  transition: opacity 140ms var(--ease-ui-out), transform 140ms var(--ease-ui-out);
}

.panel-tip.is-instant.is-open { transition: none; }

/* ── Context menu (spec §6.6) ───────────────────────────────────────────── */

.context-menu {
  position: fixed;
  z-index: 10;
  min-width: 150px;
  padding: 4px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-window-fill-strong);
  box-shadow: var(--shadow-float);
  backdrop-filter: blur(14px);
  animation: menu-in 90ms ease-out both;
}

@keyframes menu-in {
  from { opacity: 0; transform: scale(0.98); }
  to   { opacity: 1; transform: none; }
}

.menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 5px 8px;
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--color-text);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  text-align: left;
  cursor: pointer;
}

.menu-item:hover:not(:disabled) { background: var(--color-subtle-hover); }

.menu-item:disabled {
  color: var(--color-muted);
  cursor: default;
}

.menu-item.is-danger:hover:not(:disabled) {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}

/* ── Drop overlay (spec §7.5.1) ─────────────────────────────────────────── */

.drop-overlay {
  position: absolute;
  inset: 0;
  z-index: 8;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  background: color-mix(in srgb, var(--color-panel) 88%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms var(--ease-ui-out);
}

.drop-overlay span {
  padding: 10px 18px;
  border: 1px dashed var(--color-accent-border);
  border-radius: var(--radius-md);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  color: var(--color-text);
}

body.is-dropping .drop-overlay { opacity: 1; }

/* The mono voice survives ONLY where it names keys - chords are the one thing
   that is genuinely monospace content. Everything else (meta lines, headings,
   the archive row) speaks the body font in sentence case: the all-caps mono
   treatment read as boilerplate, not brand. */
.empty-key {
  font-family: var(--font-mono);
}

/* Zen Kaku's 400 is airy at these sizes (rev-2 note 2); 500 is the body voice.
   Subpixel rendering (no `antialiased` override) adds the rest of the ink on
   Windows. */
html, body { font-weight: 500; }

/* The collapse chevron points AT the docked edge (rev-2 note 6). */
body[data-panel-edge="left"] #collapse-btn svg { transform: scaleX(-1); }
