/* ============================================================
   THE WALL — the home canvas and the thread page.

   Two states on one data attribute:

   HERO   the pitch and the bar hold the room alone, centred.
   FEED   an open canvas, like a map. The composer is a landmark
          on it; the posts are cards scattered around it that
          drag, resize, pan and zoom. The camera is one transform
          on the plane — nothing here animates layout.

   The composer is dressed as the LLM input bar everyone already
   knows; the joke is unchanged, only the room got bigger.
   ============================================================ */

:root {
  --bubble: #1e1e1e;         /* a post */
  --bubble-hi: #242424;      /* under the pointer */
  --bar: #1b1b1b;            /* the composer card */
  --wall-w: 760px;
}

/* ---------------------------------------------------------------
   1. THE PAGE — nothing scrolls; the canvas moves instead
   --------------------------------------------------------------- */

.wall { height: 100dvh; overflow: hidden; }

/* ---- HERO --------------------------------------------------------
   A fixed, centred stack. pointer-events pass through the empty
   space so the canvas beneath stays reachable once it exists. */
/* Two rows. The slider takes the lower one and floats a fixed distance
   off the foot of the page; the pitch and the bar centre in everything
   above it — so their middle lands halfway between the top of the page
   and the top of the slider.

   Deliberately not circular: the slider's place is set against the
   viewport, and the stack is centred against the slider. Were each
   measured from the other, neither would have an answer. Grid derives
   both from the real heights, so nothing needs recomputing when the
   window resizes or the draft grows a line. */
.wall-hero {
  position: fixed; inset: 0; z-index: 2;
  display: grid;
  /* Three rows. The greeting and the bar take the middle one, and the
     0.82 above them against 1 below sets them just off true centre —
     the nav sits over the top of that space, and dead-centre reads a
     touch low once the eye counts the chrome as part of the ceiling.

     The feature marks then centre in the last row, and that row is
     precisely the room between the bottom of the bar and the bottom of
     the page — so their midpoint is the layout, not a number anyone has
     to maintain as the window resizes or the draft grows a line. */
  grid-template-rows: 0.82fr auto minmax(0, 1fr);
  justify-items: center;
  padding: 0 var(--pad-x);
  pointer-events: none;
}
.hero-stack {
  grid-row: 2;
  display: flex; flex-direction: column; align-items: center;
  width: 100%;
}
.wall-hero .enter,
.wall-hero .feats { grid-row: 3; align-self: center; }
/* stood down, not deleted — see about.html */
.enter[hidden] { display: none; }

/* ---- the constellation --------------------------------------------------
   The day's posts, seen from outside the canvas: one dot per thought.
   Size is how much was said, opacity is how recently — the same two
   facts the cards themselves carry, read at a distance. The layer is
   screen space and inert; it is a view of the wall, not the wall.

   The dots only exist in the hero. The moment the canvas opens they are
   the cards, and two copies of the same thought cannot stand at once. */
.dots {
  position: fixed; inset: 0; z-index: 1;
  pointer-events: none;
}
.dots i {
  position: absolute;
  border-radius: 50%;
  background: var(--t-second);
  /* the dots are doors as well as decoration — a double click enters
     the canvas parked on that card. transform/opacity are the magnet's,
     written per frame from script. */
  pointer-events: auto;
}
.wall[data-state="feed"] .dots { display: none; }

/* the traveller between the two states: a dot growing into its card, or
   a card shrinking back into its dot. Geometry and colour both animate,
   from script — this only sets what never changes. */
.dot-ghost {
  position: fixed; left: 0; top: 0; z-index: 4;
  pointer-events: none;
  will-change: left, top, width, height;
}

/* ---- the day ----------------------------------------------------------
   Which day's canvas this is. The same control twice: large in the hero
   where it is the greeting, and a quiet pill on the Return row once the
   canvas is open — so the way to change day never moves. */
.day-btn {
  display: inline-flex; align-items: center; gap: 10px;
  font: inherit; color: inherit;
  padding: 0;
}
.day-btn svg {
  width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--t-dim);
  transition: color var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.day-btn:hover svg { color: var(--t-second); }
.wall[data-cal] .day-btn svg { transform: rotate(180deg); }

/* Centred on the Return's row, not merely near it: same top, lifted by
   half the difference between the pill and the 15px mono line Return
   draws — so the two stay level if either changes size. */
.day-pill {
  position: fixed; left: 50%;
  top: calc(var(--nav-h) + 18px + (15px - 30px) / 2);
  transform: translateX(-50%);
  z-index: 6;
  display: inline-flex; align-items: center; gap: 7px;
  height: 30px; padding: 0 14px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--t-second);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition:
    opacity 280ms var(--ease),
    background var(--fast) var(--ease),
    color var(--fast) var(--ease),
    visibility 0s linear 280ms;
}
.wall[data-state="feed"] .day-pill {
  opacity: 1; visibility: visible; pointer-events: auto;
  transition-delay: 260ms, 0s, 0s, 0s;
}
.day-pill:hover { background: rgba(255, 255, 255, 0.1); color: var(--t-primary); }
.day-pill svg {
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ---- the calendar -----------------------------------------------------
   A rail of months running left to right, oldest first, with today's
   month at the far right — so scrolling back through it is scrolling
   back through time, and the gesture matches the direction. */
.cal {
  position: fixed; inset: 0; z-index: 30;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  background: rgba(0, 0, 0, 0.94);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: cal-in 220ms var(--ease) both;
}
.cal[hidden] { display: none; }
@keyframes cal-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .cal { animation: none; } }

.cal-rail {
  display: flex; align-items: center;
  gap: 110px;
  padding: 0 max(var(--pad-x), calc(50vw - 240px));
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.cal-rail::-webkit-scrollbar { display: none; }

.cal-month {
  flex: none;
  scroll-snap-align: center;
  /* the month either side reads as context, not as a choice */
  opacity: 0.32;
  transition: opacity 280ms var(--ease);
}
.cal-month[data-near] { opacity: 1; }

.cal-title {
  font-family: var(--sans);
  font-size: 26px; letter-spacing: -0.01em;
  text-align: center;
  color: var(--t-primary);
  margin-bottom: var(--s5);
}

.cal-grid { display: grid; grid-template-columns: repeat(7, 62px); gap: 8px; }

/* the weekday header — pills, so the row reads as a legend rather than
   as another row of days */
.cal-wd {
  display: grid; place-items: center;
  height: 30px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--t-second);
}

.cal-day {
  position: relative;
  display: grid; place-items: center;
  height: 62px;
  border-radius: 16px;
  background: #1e1e1e;
  font-family: var(--sans); font-size: 16px;
  color: var(--t-second);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
button.cal-day:hover { background: #262626; color: var(--t-primary); }
.cal-day[data-empty] { background: none; pointer-events: none; }

/* a day that has not happened keeps its place in the month but is not a
   door — nothing can have been written there yet */
.cal-day[data-ahead] { background: none; color: var(--t-dim); }

/* a day that has thoughts on it carries a mark; an empty day carries
   nothing, so the month reads as a record at a glance */
.cal-day[data-has]::before {
  content: '';
  position: absolute; top: 12px; left: 50%;
  width: 4px; height: 4px; margin-left: -2px;
  border-radius: 50%;
  background: var(--t-second);
}
.cal-day[data-has] { padding-top: 10px; }

.cal-day[aria-current="date"] {
  box-shadow: inset 0 0 0 1px var(--paper);
  color: var(--t-primary);
}

.cal-close {
  justify-self: center;
  margin-bottom: 64px;
  height: 38px; padding: 0 24px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--t-second);
  font-family: var(--sans); font-size: 15px;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.cal-close:hover { background: rgba(255, 255, 255, 0.13); color: var(--t-primary); }

/* ---- the feature marks ------------------------------------------------
   The same language the case-studies pager speaks: 6px squares, solid
   greys rather than translucent whites, and a name held back until the
   pointer asks for it. This page is not one thing but a set of them, and
   the marks are how you cross between.

   A reserved mark is drawn at half weight and answers nothing — better
   an honest blank than a control that lies about being ready. */
.feats {
  display: flex; align-items: center;
  --mark: 6px;
  --mark-rest: #4f4f4f;
  --mark-hot: #9c9c9c;
  transition: opacity 280ms var(--ease), visibility 0s linear 0s;
}
.wall[data-state="feed"] .feats {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 280ms var(--ease), visibility 0s linear 280ms;
}

/* the button is the hit target and nothing else; the mark it stands for
   is drawn inside it, centred */
.feat {
  position: relative;
  display: grid; place-items: center;
  width: 24px; height: 44px;
}
.feat i {
  width: var(--mark); height: var(--mark);
  background: var(--mark-rest);
  transition: background var(--mid) var(--ease);
}
.feat:hover i, .feat:focus-visible i { background: var(--mark-hot); }
.feat[aria-current="true"] i { background: var(--paper); }
/* A reserved mark is a place you can stand, so it answers the pointer
   like the others — just at half weight, and with no name to give. */
.feat[data-reserved] i { background: #2c2c2c; }
.feat[data-reserved]:hover i,
.feat[data-reserved]:focus-visible i { background: #3f3f3f; }
.feat[data-reserved][aria-current="true"] i { background: var(--mark-rest); }

/* the name is held back until asked for — hovering a mark is the
   question. It sits above the row, out of the way of what it names. */
.feat-name {
  position: absolute; bottom: calc(50% + 12px); left: 50%;
  transform: translate(-50%, 4px);
  color: var(--t-dim);
  white-space: nowrap; pointer-events: none;
  opacity: 0;
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
}
.feat:hover .feat-name,
.feat:focus-visible .feat-name {
  opacity: 1; transform: translate(-50%, 0);
  color: var(--t-second);
}

/* ---- a reserved page ---------------------------------------------------
   Selecting a mark that has nothing behind it yet takes the page away
   and leaves the marks, because the marks are the only part that is
   about the set rather than about one member of it. Nothing is put in
   the space: an empty room says "not yet" more honestly than a label
   promising something that does not exist.

   The stack is faded rather than removed so the crossing reads as a
   move between pages, not as the page breaking. */
.wall:not([data-feature="notes"]) .hero-stack {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 240ms var(--ease), visibility 0s linear 240ms;
}
.hero-stack {
  transition: opacity 240ms var(--ease), visibility 0s linear 0s;
}

/* The hero is a see-through frame — it covers the window so its grid can
   place things, and takes no pointer for itself. Everything in it that is
   actually a control has to be handed the pointer back by name. The marks
   are a direct child of the frame, not of the stack, so they need saying
   separately; without this they draw but answer nothing. */
.hero-stack > *, .wall-hero .enter, .feats { pointer-events: auto; }
.wall-hero .composer { width: min(var(--wall-w), 100%); }

.wall-title {
  font-family: var(--sans);
  font-size: 26px; line-height: 1.2; letter-spacing: -0.01em;
  text-align: center;
  color: var(--t-primary);
  margin-bottom: var(--s4);
  transition: opacity 280ms var(--ease), transform 280ms var(--ease);
}
.wall[data-state="feed"] .wall-title {
  /* the entry animation's fill would hold opacity at 1 forever and
     outrank this fade — in FEED it has long finished its job */
  animation: none;
  opacity: 0; transform: translateY(-5px);
  pointer-events: none;
}
/* the composer leaves the hero for the canvas; what stays cannot catch
   a single click */
.wall[data-state="feed"] .wall-hero { pointer-events: none; }
.wall[data-state="feed"] .wall-hero *,
.wall[data-state="feed"] .wall-hero > * { pointer-events: none; }

/* ---- slide to enter -------------------------------------------------
   The other door onto the wall. One pill: the runner is the thumb at
   the left, the label sits in the track, the chevron is the direct
   route. Dragging the runner to the far edge opens the canvas. */
.enter {
  display: flex; align-items: stretch;
  height: 52px;
  border-radius: 14px;
  background: var(--bar);
  overflow: hidden;
  transition: opacity 280ms var(--ease), visibility 0s linear 0s;
}
.wall[data-state="feed"] .enter {
  animation: none;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 280ms var(--ease), visibility 0s linear 280ms;
}

.enter-track {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 250px;
  touch-action: none;              /* the slide is ours, not the page's */
}

.enter-label {
  /* the runner needs room; the label yields as he approaches */
  padding-left: 18px;
  /* the hint breathes: a slow loop from dim to a lighter grey and back.
     It animates colour, not opacity — the slide's own fade owns opacity,
     and an animation would outrank the inline value it writes. */
  color: var(--t-dim);
  animation: enter-hint 2600ms var(--ease-io) infinite;
  transition: opacity var(--fast) var(--ease);
}
@keyframes enter-hint {
  0%, 100% { color: rgba(255, 255, 255, 0.3); }
  50% { color: rgba(255, 255, 255, 0.72); }
}
@media (prefers-reduced-motion: reduce) {
  .enter-label { animation: none; }
}

.enter-man {
  position: absolute; left: 12px; top: 50%;
  width: 28px; height: 28px;
  margin-top: -14px;
  display: grid; place-items: center;
  color: var(--t-second);
  cursor: grab;
  will-change: transform;
  transition: color var(--fast) var(--ease);
}
.enter-man svg { width: 16px; height: 21px; }
.enter-track:hover .enter-man { color: var(--t-primary); }

/* the slide lands: the runner leaves through the right edge */
.enter.done .enter-man {
  transition: transform 220ms var(--ease-in), opacity 220ms var(--ease-in);
  opacity: 0;
}

.enter-go {
  display: grid; place-items: center;
  width: 48px;
  border-left: 1px solid var(--hair-soft);
  color: var(--t-dim);
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.enter-go:hover { color: var(--t-primary); background: var(--btn-bg); }
.enter-go svg {
  width: 15px; height: 15px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}

/* the pitch, the bar and the door arrive together; CSS only, like the
   contact page, so the entry cannot depend on a script that may not run */
.wall-title { animation: wall-in 520ms var(--ease) both; }
.wall-hero .composer { animation: wall-in 520ms var(--ease) 90ms both; }
.wall-hero .enter { animation: wall-in 520ms var(--ease) 180ms both; }
@keyframes wall-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .wall-title, .wall-hero .composer, .wall-hero .enter { animation: none; }
}

/* ---------------------------------------------------------------
   2. THE CANVAS
   --------------------------------------------------------------- */

.canvas {
  position: fixed; inset: 0; z-index: 1;
  overflow: hidden;
  /* every gesture on it — pan, pinch, drag — is ours, not the page's */
  touch-action: none;
  overscroll-behavior: contain;
}
.wall[data-state="hero"] .canvas { display: none; }

.plane {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}

/* ---- wires ---------------------------------------------------------
   Connections, drawn under everything. The svg is a 1px stake at the
   plane's origin with overflow left visible, so a line can run to any
   coordinate; non-scaling strokes hold a hairline at every zoom. */
/* A real canvas, not a 1px stake. base.css caps every svg at max-width
   100%, and the plane has no width of its own — so a hairline svg here
   computed to zero and clipped away the very lines it was drawing. It
   gets a large square instead, offset to match its viewBox so lines can
   be given plain plane coordinates. */
/* The svg is a 20,000px box laid over the whole plane, so it must not
   be hit-testable itself — it would swallow every press on open ground
   and the canvas would never see one. The hit lines opt back in below;
   a descendant may override an inherited `none`. */
#wires {
  position: absolute; top: -10000px; left: -10000px;
  width: 20000px; height: 20000px;
  max-width: none;
  overflow: visible;
  pointer-events: none;
  transition: opacity 200ms var(--ease);
}
/* Only the stretch between two cards is ever on show — the rest runs
   under them — so this has to carry at a couple of hundred pixels of
   line on black. --hair is a divider's weight and vanishes here. */
#wires line {
  stroke: rgba(255, 255, 255, 0.34);
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
}
/* a thread's own lines — the same language, a step quieter than a
   connection tied by hand, but still plainly a line between two things.
   Not yours to cut, so they take no pointer either. */
/* dashed, because a reply is a different kind of tie from one made by
   hand — the wall's own relationship rather than the visitor's */
#wires line.wire--reply {
  stroke: rgba(255, 255, 255, 0.24);
  stroke-dasharray: 4 6;
  pointer-events: none;
}

/* the hit area: a hairline is not a target, so every connection carries
   a fat invisible twin to be grabbed by */
#wires line.wire-hit {
  stroke: transparent;
  stroke-width: 16;
  vector-effect: non-scaling-stroke;
  cursor: pointer;
}
#wires line.wire { pointer-events: none; }

/* These need the id too. The base rule above is `#wires line`, and an id
   outranks any number of classes — without the same prefix, hover and
   selection would lose to it and never paint. */
#wires .wire-g:hover line.wire { stroke: rgba(255, 255, 255, 0.62); }
#wires .wire-g[data-sel] line.wire {
  stroke: var(--paper);
  stroke-width: 2.2;
}

/* the composer as a landmark: same card, absolutely placed by feed.js */
.plane .composer {
  position: absolute;
  transition: opacity 280ms var(--ease), visibility 0s linear 0s;
}

/* ---- an open thread -----------------------------------------------
   The rest of the wall stands aside — faded, unfocusable, untouchable —
   while the opened post and its orbit have the floor. visibility rides
   behind the fade so nothing invisible can catch a Tab stop. */
.wall[data-focus] .plane .composer,
.wall[data-focus] #nodes .node:not([data-focused]):not([data-linked]) {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 280ms var(--ease), visibility 0s linear 280ms;
}

/* what the open post is wired to stays — a step back from the post
   itself, so the thread still holds the eye */
.wall[data-focus] #nodes .node[data-linked] { opacity: 0.55; }
.wall[data-focus] #nodes .node[data-linked]:hover { opacity: 1; }

/* the post with the floor — lifted a shade, exactly like a hover */
.node[data-focused] { background: var(--bubble-hi); }

/* ---- a card --------------------------------------------------------
   A post at a place, with a width. Dragging writes left/top; the grip
   writes width; the transform is only ever the arrival animation. */
.node {
  position: absolute;
  /* the floor is the counters row, not a look — width comes from the
     content, measured in feed.js. The ceiling is the clamp on .node-text
     plus this card's own furniture. */
  min-width: 190px; max-width: 560px;
  max-height: 260px;
  padding: 16px 18px 12px;
  border-radius: 20px;
  background: var(--bubble);
  user-select: none;
  -webkit-user-select: none;
  transition: background var(--fast) var(--ease);
}
.node:hover { background: var(--bubble-hi); }

/* scattering in: scale up from almost-there, staggered by --i.
   Only opacity and transform — never left/top. */
.node {
  opacity: 0; transform: scale(0.94) translateY(8px);
}
.node[data-in="true"] {
  opacity: 1; transform: none;
  transition:
    opacity 420ms var(--ease-soft) calc(var(--i, 0) * 45ms),
    transform 420ms var(--ease-soft) calc(var(--i, 0) * 45ms),
    background var(--fast) var(--ease),
    /* coming back to full under the hand, and standing down again */
    filter 240ms var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .node, .node[data-in="true"] { opacity: 1; transform: none; transition: none; }
}

/* ---- age ---------------------------------------------------------
   How old a thought is, read as how present it is. --age is written per
   card in feed.js from the time it was written.

   filter, not opacity: opacity is the arrival animation's, and a card
   scattering in from 0 must not also be arguing about how old it is.
   The two multiply, which is exactly right — a fresh card arrives to
   full strength and an old one arrives already stood back.

   At rest only. Hovering, selecting, or opening a card brings it all
   the way back: this is a ranking of what is recent, not a claim that
   the older ones are less worth reading. */
.node { filter: opacity(var(--age, 1)); }
.node:hover,
.node[data-picked],
.node[data-focused],
.node[data-train] { filter: none; }

/* The gravity crush: the packed layout is computed in one pass, and
   this short glide is how the cards get to it. Removed a beat later so
   drags never feel laggy. */
#nodes.packing .node {
  transition: left 260ms var(--ease-soft), top 260ms var(--ease-soft);
}

.node-by { display: block; color: var(--t-dim); margin-bottom: var(--s2); }

/* A card is a glance, not the whole document — past a certain depth it
   stops being a card on a wall and starts being a wall of its own. The
   thread is where a long post is read in full, so the face of it is cut
   off at seven lines with an ellipsis. */
.node-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 7;
  line-clamp: 7;
  overflow: hidden;
  font-family: var(--sans); font-size: 15px; line-height: 1.55;
  color: var(--t-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.node-meta {
  display: flex; align-items: center; gap: var(--s4);
  margin-top: 14px;
  font-variant-numeric: tabular-nums;
}
.node-when { margin-left: auto; color: var(--t-dim); }

/* ---- a reply in orbit ---------------------------------------------
   The thread's satellites: smaller, darker, one notch quieter than the
   post they circle. Same drag, same grip, tighter width limits. */
.node--reply {
  min-width: 180px; max-width: 340px;
  padding: 12px 15px 10px;
  border-radius: 16px;
  background: #191919;
}
.node--reply:hover { background: #1e1e1e; }
.node--reply .node-by { margin-bottom: 6px; }

.reply-text {
  font-family: var(--sans); font-size: 13.5px; line-height: 1.5;
  color: var(--t-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* the resize grip — a quiet corner that only introduces itself on hover */
.node-grip {
  position: absolute; right: 4px; bottom: 4px;
  width: 18px; height: 18px;
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
}
.node-grip::before {
  content: '';
  position: absolute; right: 4px; bottom: 4px;
  width: 8px; height: 8px;
  border-right: 1px solid var(--t-dim);
  border-bottom: 1px solid var(--t-dim);
  border-radius: 0 0 3px 0;
}
.node:hover .node-grip { opacity: 1; }

/* ---------------------------------------------------------------
   3. SCREEN-SPACE CHROME — Return, and the zoom readout
   --------------------------------------------------------------- */

.wall-back {
  position: fixed; top: calc(var(--nav-h) + 18px); left: var(--pad-x);
  z-index: 6;
  margin-bottom: 0;                 /* .back carries one; not in a flow here */
  opacity: 0; transform: translateY(5px);
  pointer-events: none;
  transition:
    opacity 280ms var(--ease),
    transform 280ms var(--ease),
    color var(--fast) var(--ease);  /* .back's own hover, kept */
}
.wall[data-state="feed"] .wall-back {
  opacity: 1; transform: none;
  pointer-events: auto;
  transition-delay: 260ms, 260ms, 0s;
}

/* ---- the marquee ------------------------------------------------------
   Screen space, not plane space: it is drawn where the hand is, not
   where the wall is, so it does not stretch or thin out with the zoom. */
.marquee {
  position: fixed; z-index: 7;
  border: 1px solid rgba(255, 255, 255, 0.55);
  background: rgba(255, 255, 255, 0.06);
  border-radius: 2px;
  pointer-events: none;
}

/* a card the marquee caught */
.node[data-picked] {
  background: var(--bubble-hi);
  box-shadow: 0 0 0 1px var(--t-second);
}

/* ---- the canvas toolbar ----------------------------------------------
   Screen space, along the foot of the wall. Two modes on the left (what
   a drag does), two actions in the middle, connect on the right —
   grouped so the eye can tell a mode from a one-off press without
   reading a single label. */
.toolbar {
  position: fixed; left: 50%; bottom: 24px;
  transform: translate(-50%, 8px);
  z-index: 6;
  display: flex; align-items: center; gap: 6px;
  padding: 6px;
  border-radius: 14px;
  background: #1b1b1b;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity 280ms var(--ease),
    transform 280ms var(--ease),
    visibility 0s linear 280ms;
}
.wall[data-state="feed"] .toolbar {
  opacity: 1; transform: translate(-50%, 0);
  visibility: visible;
  pointer-events: auto;
  transition: opacity 280ms var(--ease) 200ms, transform 280ms var(--ease) 200ms,
    visibility 0s linear 0s;
}
/* a thread has the reply bar down here instead */
.wall[data-focus] .toolbar {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 200ms var(--ease), visibility 0s linear 200ms;
}

/* the buttons in a set stand 8px apart — enough air that each icon
   reads as its own control rather than as one segmented switch */
.tool-set { display: flex; align-items: center; gap: 8px; }

.tool-sep {
  width: 1px; height: 22px;
  background: var(--hair-soft);
  margin-inline: 4px;
}

.tbtn {
  display: grid; place-items: center;
  width: 32px; height: 32px;
  border-radius: 8px;
  color: var(--t-dim);
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.tbtn:hover { color: var(--t-primary); background: var(--btn-bg); }

/* a held mode reads as held: filled, and at full strength */
.tbtn[aria-pressed="true"] {
  background: var(--paper);
  color: var(--ink);
}

.tbtn svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}
/* the pointer is a solid shape, not an outline */
.tbtn[data-tool="move"] svg { fill: currentColor; stroke-width: 1; }
/* the hand is drawn on a 19-unit box where everyone else uses 16 —
   its stroke scales down with it, so it gets the difference back */
.tbtn[data-tool="hand"] svg { stroke-width: 1.65; }

/* ---- the reply bar --------------------------------------------------
   Screen space, along the bottom, only while a thread is open. It
   arrives a beat after the orbit so the post gets read first. The
   visibility flip rides behind the fade — nothing invisible may hold
   a Tab stop. */
.reply-bar {
  position: fixed; left: 50%; bottom: 28px;
  transform: translate(-50%, 8px);
  width: min(760px, calc(100vw - 2 * var(--pad-x)));
  margin: 0;
  z-index: 6;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    opacity 280ms var(--ease),
    transform 280ms var(--ease),
    visibility 0s linear 280ms;
}
.wall[data-focus] .reply-bar {
  opacity: 1; transform: translate(-50%, 0);
  visibility: visible;
  pointer-events: auto;
  transition: opacity 280ms var(--ease) 200ms, transform 280ms var(--ease) 200ms,
    visibility 0s linear 0s;
}

/* no leading tool on this row — the field itself carries the inset the
   thread page gets from its + button. Doubled class to outrank the
   row's own padding shorthand, which is declared later. */
.composer--row.reply-bar .composer-input { padding-left: 18px; }

/* ---------------------------------------------------------------
   4. THE COMPOSER — an input bar in an input bar's clothing
   --------------------------------------------------------------- */

.composer {
  padding: 20px 20px 14px;
  border-radius: 26px;
  background: var(--bar);
}

.composer-input {
  display: block;
  width: 100%;
  border: 0; padding: 2px 4px;
  background: transparent;
  color: var(--t-primary);
  font-family: var(--sans); font-size: 15px; line-height: 1.5;
  resize: none;
  /* hidden until the draft actually outgrows its cap — grow() flips it.
     Otherwise a wrapped placeholder ships a scrollbar with nothing in it. */
  overflow-y: hidden;
  min-height: 44px;
}
.composer-input::placeholder { color: var(--t-dim); }
.composer-input:focus { outline: none; }

.composer-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3);
  margin-top: 10px;
}

.composer-tools { display: flex; align-items: center; gap: var(--s2); }

/* attach and link — present, honest about being props */
.tool {
  display: grid; place-items: center;
  width: 32px; height: 32px;
  border-radius: 8px;
  color: var(--t-dim);
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.tool:hover { color: var(--t-primary); background: var(--btn-bg); }

/* the train is a toggle and has to read as held down, not as hovered —
   it stays lit while it is on, through the pointer leaving and back */
.tool[aria-pressed="true"] {
  color: var(--ink);
  background: var(--paper);
}
.tool[aria-pressed="true"]:hover { color: var(--ink); background: var(--paper); }
.tool svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round;
}

.composer-act { display: flex; align-items: center; gap: var(--s3); }

/* the one real control. Dimmed and inert until there is a draft —
   a Post button with nothing to post is a decoration. */
.post-btn {
  height: 36px; padding: 0 24px;
  border-radius: 999px;
  background: var(--paper); color: var(--ink);
  font-family: var(--sans); font-size: 14px;
  opacity: 0.4; pointer-events: none;
  transition: opacity var(--fast) var(--ease);
}
.composer[data-ready] .post-btn { opacity: 1; pointer-events: auto; }
.composer[data-ready] .post-btn:hover { opacity: 0.88; }

/* ---- the door ---------------------------------------------------------
   Drop and Slide to enter are the same slot, so they are laid in the
   same grid cell rather than swapped in and out of the flow. Two things
   follow, and both are the point:

     · the cell is as wide as the wider of them, so changing door moves
       nothing else in the bar — no shunt of the Work chip as you type
       the first character
     · they can be crossfaded, because both are always present

   filter carries the fade, not opacity: Drop already uses opacity to
   say whether there is anything to drop, and the two meanings must not
   share a property. visibility rides behind the fade so nothing you
   cannot see can still be tabbed to or pressed. */
/* The slot takes the width of whichever door is showing, so there is
   never dead air between Work and the control — the two natural widths
   are measured once in feed.js and the swap glides between them,
   carrying the visibility chip along on the flex row. Anchored at the
   right, which the bar's space-between holds still, so only the left
   edge travels. */
.door {
  display: grid; justify-items: end; align-items: center;
  /* the column must be the slot, not the widest door: an auto track
     sizes to max-content and overhangs, and `end` then aligns to the
     TRACK's edge — which put Drop outside the bar entirely */
  grid-template-columns: minmax(0, 1fr);
  /* flex must not shrink it, and must not floor it either: a flex
     item's automatic minimum is its min-content width, which here is
     the WIDER door — so the narrow state could never take effect and
     the slot stayed stuck at Slide's size. */
  flex: none;
  min-width: 0;
  width: var(--w-post, auto);
  transition: width 240ms var(--ease);
}
.door[data-door="slide"] { width: var(--w-slide, auto); }
.door > * {
  grid-area: 1 / 1;
  /* the slot is sized from a rounded-down measurement, so it can land a
     fraction of a pixel under what the label needs; a door that wraps
     its own name is worse than one that overhangs by half a pixel */
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .door { transition: none; }
}

.door > * {
  filter: opacity(0);
  visibility: hidden;
  transition: filter 180ms var(--ease), visibility 0s linear 180ms;
}
.door[data-door="post"] .post-btn,
.door[data-door="slide"] .slide {
  filter: none;
  visibility: visible;
  transition: filter 180ms var(--ease), visibility 0s linear 0s;
}

/* ---- slide to enter, in the bar ---------------------------------------
   The Post button's understudy: same row, same height, so swapping one
   for the other moves nothing else in the bar. It reads as a track
   rather than a button — a thumb parked at one end and room to its
   right — because it costs a gesture, not a click. */
.slide {
  position: relative;
  display: flex; align-items: center; gap: 12px;
  height: 36px; padding: 0 20px 0 6px;
  border-radius: 999px;
  background: var(--btn-bg);
  font-family: var(--sans); font-size: 14px;
  color: var(--t-second);
  touch-action: none;               /* the slide is ours, not the page's */
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.slide:hover { color: var(--t-primary); }

.slide-thumb {
  width: 18px; height: 24px; flex: none;
  border-radius: 7px;
  background: var(--paper);
  cursor: grab;
  will-change: transform;
}
.slide:active .slide-thumb { cursor: grabbing; }

/* the label yields as the thumb comes across — set from script, which
   knows how far along the run the hand is */
.slide-label { transition: opacity var(--fast) linear; }

/* at the end of the run but the hand still down: it will open when the
   hand lets go, and the track says so before it happens */
.slide[data-armed] { background: rgba(255, 255, 255, 0.16); }

/* The nob growing into Drop. Screen space and out of the flow, because
   it spans two elements in two different coordinate systems — the
   slide's, which is about to vanish, and the canvas bar's, which does
   not exist yet at the moment the run finishes. */
.slide-ghost {
  position: fixed; left: 0; top: 0; z-index: 20;
  display: grid; place-items: center;
  background: var(--paper); color: var(--ink);
  font-family: var(--sans); font-size: 14px;
  white-space: nowrap; overflow: hidden;
  pointer-events: none;
}

/* ---- the visibility menu ---------------------------------------- */

.vis { position: relative; }

.vis-btn {
  display: flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 8px;
  border-radius: 8px;
  color: var(--t-second);
  font-family: var(--sans); font-size: 14px;
  transition: color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.vis-btn:hover { color: var(--t-primary); background: var(--btn-bg); }
.vis-btn svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round;
  transition: transform var(--fast) var(--ease);
}
/* the chevron reports the menu's state; the tag is the chip's subject
   and holds still — scoped to the last mark, or both would turn */
.vis-btn[aria-expanded="true"] > svg:last-of-type { transform: rotate(180deg); }
.vis-tag { stroke-linejoin: round; }

/* drops beneath the chip, the way the control it is dressed as does */
.vis-menu {
  position: absolute; right: 0; top: calc(100% + 6px);
  min-width: 190px;
  padding: 6px;
  border-radius: 12px;
  background: #242424;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  z-index: 5;
}

.vis-opt {
  padding: 9px 12px;
  border-radius: 8px;
  font-family: var(--sans); font-size: 14px;
  color: var(--t-second);
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.vis-opt:hover { background: var(--btn-bg); color: var(--t-primary); }
.vis-opt[aria-selected="true"] { color: var(--t-primary); }

/* ---- the two faces ------------------------------------------------
   One box, one face at a time: the tags, or the field that names a new
   one. Both are laid out the same way so the box does not jump when it
   changes what it is showing. */

.vis-list { display: grid; gap: 2px; }

/* the action sits above the values it adds to, and a hairline keeps it
   from reading as one of them */
.vis-add {
  display: flex; align-items: center; gap: 9px;
  width: 100%;
  margin-bottom: 6px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--hair-soft);
  border-radius: 8px 8px 0 0;
  text-align: left;
}
.vis-add svg {
  width: 13px; height: 13px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round;
}

/* naming a tag: a field with nothing drawn around it — the box it sits
   in is already the frame */
.vis-input {
  width: 100%;
  height: 36px;
  padding: 0 12px;
  border: 0;
  background: none;
  font-family: var(--sans); font-size: 14px;
  color: var(--t-primary);
}
.vis-input::placeholder { color: var(--t-faint); }
.vis-input:focus { outline: none; }

.vis-foot {
  display: flex; justify-content: flex-end;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--hair-soft);
}
.vis-cancel {
  height: 28px; padding: 0 10px;
  border-radius: 7px;
  font-family: var(--sans); font-size: 13px;
  color: var(--t-dim);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.vis-cancel:hover { color: var(--t-primary); background: var(--btn-bg); }

/* the same menu, worn by a right-click: pinned in screen space at the
   pointer instead of hanging off the composer's chip */
.vis-menu.ctx {
  position: fixed;
  right: auto; top: auto;
  min-width: 150px;
  z-index: 30;
}

/* ---- making a wire -------------------------------------------------
   The tied end wears a ring; while a wire is in hand, every card the
   pointer crosses offers itself the same way. */
.node[data-link],
.wall[data-linking] #nodes .node:hover {
  box-shadow: 0 0 0 1px var(--t-second);
}

/* The head of a train of thought wears the same ring, in full white:
   this is the card the next post will couple to, and unlike a wire in
   hand it stays on — so it has to hold its own against the wall. */
.node[data-train] { box-shadow: 0 0 0 1px var(--paper); }

/* ---------------------------------------------------------------
   5. SHARED COUNTERS — the wall's cards and the thread's post
   --------------------------------------------------------------- */

.msg-meta {
  display: flex; align-items: center; gap: var(--s4);
  font-variant-numeric: tabular-nums;
}

.msg-like, .msg-replies {
  display: inline-flex; align-items: center; gap: 7px;
  font-family: var(--sans); font-size: 13px;
  color: var(--t-second);
  transition: color var(--fast) var(--ease);
}
.msg-like:hover, a.msg-replies:hover { color: var(--t-primary); }
.msg-like svg, .msg-replies svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.3;
  stroke-linejoin: round;
}

/* a held like fills; the count reads as yours-included */
.msg-like.liked { color: var(--t-primary); }
.msg-like.liked svg { fill: currentColor; }

/* the press answers — one pop, only on the way in */
.msg-like.pop svg { animation: like-pop 360ms var(--ease); }
@keyframes like-pop {
  0% { transform: scale(1); }
  35% { transform: scale(1.3); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .msg-like.pop svg { animation: none; } }

/* held back — a dot and the word, in the same quiet register as a
   byline. It marks the post rather than decorating it. */
.msg-priv { display: inline-flex; align-items: center; gap: 6px; }
.msg-priv::before {
  content: '';
  width: 5px; height: 5px; border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* a page with nothing to show says so — the thread's missing-post line */
.feed-empty { color: var(--t-dim); padding-block: var(--s4); }

/* ---------------------------------------------------------------
   6. THE THREAD PAGE
   --------------------------------------------------------------- */

.thread {
  max-width: calc(680px + 2 * var(--pad-x));
  margin-inline: auto;
  padding: calc(var(--nav-h) + var(--s6)) var(--pad-x) var(--s7);
  min-height: calc(100dvh - var(--nav-h));
}
.thread .back { margin-bottom: var(--s5); }

.op {
  position: relative;
  padding: 24px 26px 18px;
  border-radius: 22px;
  background: var(--bubble);
}

/* Out of the flow, pinned to the card's corner. In the flow it needed a
   row of its own, and on an anonymous post that row was empty all the
   way across — a band of nothing holding the text down. */
.op-when {
  position: absolute; top: 24px; right: 26px;
  color: var(--t-dim);
}

/* only rendered when there is something to say; see label() in thread.js */
.op-head {
  display: flex; align-items: center;
  color: var(--t-dim);
  margin-bottom: var(--s3);
}

.op-text {
  /* keeps the first line clear of the pinned time, which no longer
     reserves a row for itself */
  padding-right: 52px;
  font-family: var(--sans); font-size: 17px; line-height: 1.55;
  color: var(--t-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.op .msg-meta { margin-top: var(--s4); }

.replies { display: flex; flex-direction: column; gap: var(--s4); }

.reply { max-width: 85%; }
.reply--mine { margin-left: auto; text-align: right; }
.reply-by { display: block; color: var(--t-dim); margin-bottom: var(--s1); }
.reply-bubble {
  display: inline-block;
  padding: 11px 16px;
  border-radius: 16px;
  background: #191919;
  font-family: var(--sans); font-size: 14px; line-height: 1.55;
  color: var(--t-primary);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  text-align: left;
}

/* ---- the one-row bar ---------------------------------------------
   Everything on a single line: the attach prop, the field, the
   visibility, and one round control at the end. It sits between
   the post and its replies, so it wants air on both sides. */
.composer--row {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s5) 0;
  padding: 7px 7px 7px 8px;
  border-radius: 24px;
}
.composer--row .composer-input {
  flex: 1; min-width: 0;
  min-height: 24px;
  padding: 3px 4px;
  font-size: 14px;
}

/* The control is an icon now, not a word — at this size a labelled
   pill would be the widest thing on the row and read as the subject.
   Same dimmed-until-ready rule as the Post button. */
.send-btn {
  display: grid; place-items: center;
  width: 34px; height: 34px; flex: none;
  border-radius: 50%;
  background: var(--paper); color: var(--ink);
  opacity: 0.4; pointer-events: none;
  transition: opacity var(--fast) var(--ease);
}
.composer[data-ready] .send-btn { opacity: 1; pointer-events: auto; }
.composer[data-ready] .send-btn:hover { opacity: 0.88; }
.send-btn svg {
  width: 16px; height: 16px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

/* ---------------------------------------------------------------
   7. NARROWER
   --------------------------------------------------------------- */

@media (max-width: 720px) {
  .wall-title { font-size: 22px; }
  .node { min-width: 200px; }
  .reply { max-width: 94%; }
}
