/*==============================================================================
  sprawl.css — the Sprawl look, as free-standing components.

  Written to be JOINED WITH WARP'S CSS: every rule here is a plain class, never
  scoped under a wrapper. `.led`, `.provider`, `.entity` work wherever they land
  — inside a Warp app, inside index.html, inside a mirrored page — because none
  of them depends on an ancestor.

  WHAT IS DELIBERATELY MISSING
  No panel, window, popup, button, input, radio or checkbox rules. Warp owns
  those, and a second opinion about what a button looks like is exactly the kind
  of duplication that makes two stylesheets impossible to merge later. So the
  components here never paint their own frame either: no outer background,
  border or radius on `.provider`, `.crawled`, `.header` — that chrome is the
  panel's job. They only lay out and colour their own contents.

  SIZING IS ONE KNOB
  Everything is in `em`, so a component takes the size of whatever it is dropped
  into. Set `font-size` on the host panel and the whole thing scales; there are
  no pixel sizes to chase. The two exceptions are hairlines (1px borders) and
  the progress bar's track, which should not thicken with the type.

  COLOURS ARE VARIABLES, named for what they ARE, not for where they happen to
  be used first — a name like `--extractionPanelBg` is a lie the moment the
  colour is reused. Surfaces are ordered darkest to lightest so a new one has an
  obvious place to sit.
==============================================================================*/

:root {
  /* ── surfaces, darkest to lightest ─────────────────────────────────────── */
  --bgDarkest:   #161616;   /* wells: inputs, the chart ground */
  --bgPage:      #191919;   /* the page behind everything */
  --panelBg:     #1e1e1e;   /* a card or panel */
  --bgLighter:   #242424;   /* a strip inside a panel: stats, headers */
  --bgHoverSoft: #262626;   /* row hover */
  --bgHover:     #2d2d2d;   /* cell hover */

  /* ── lines ─────────────────────────────────────────────────────────────── */
  --lineSoft:    #262626;   /* between rows of one list */
  --line:        #2e2e2e;   /* the standard divider and border */
  --lineStrong:  #3a3a3a;   /* a divider that has to be noticed */

  /* ── type, brightest to quietest ───────────────────────────────────────── */
  --text:        #e8e8e8;   /* headings, values, the number in a stat */
  --textDim:     #c8c8c8;   /* body copy */
  --textMuted:   #8a8a8a;   /* labels */
  --textFaint:   #6a6a6a;   /* captions, section heads */
  --textGhost:   #5a5a5a;   /* ids, empty states */

  /* ── meaning ───────────────────────────────────────────────────────────── */
  --accent:      #6c9bd0;   /* links, and a count that went somewhere */
  --ok:          #5fbf6a;   /* running, healthy */
  --warn:        #d1a86a;   /* parked, stale, nearly spent */
  --danger:      #d1706a;   /* blocked, failing, spent */
  --barBlue:     #6a8fd1;   /* a progress bar with nothing wrong */

  /* ── entity kinds — one hue each, used by .entity ──────────────────────── */
  --person:      #7fb2e5;
  --group:       #c08fe0;
  --place:       #5fbf6a;
  --event:       #d1a05a;
  --work:        #e0c060;
  --fact:        #7fc98a;

  /* ── unlit LEDs ────────────────────────────────────────────────────────── */
  /* The same hue with the light off, not grey: an off green lamp still reads as
     the green one, which is what makes a row of them scannable. */
  --ledOff:      #3a3a3a;
  --ledOffGreen: #294a2d;
  --ledOffRed:   #4a2a28;
  --ledOffAmber: #4a3a24;
  --ledOffBlue:  #2a3c50;

  /* ── type scale ────────────────────────────────────────────────────────── */
  /* Six steps, all relative to the panel's own font-size, so one `font-size` on
     the host still scales everything. At the dashboard's 19.5px they land on
     23.4 / 20.5 / 14 / 13.3 / 11.7 / 10.1px.

     THEY ARE `em`, SO THEY MULTIPLY. Apply a step at a COMPONENT ROOT and let
     its children stay relative to that root — never nest two steps, or you get
     the 0.68 × 0.68 = 9px accident this file exists to avoid. Every component
     below sets its own step exactly once, which is why none of them need a
     font-size in the markup. */
  --fsBig:    1.2em;    /* the number in a stat */
  --fsTitle:  1.05em;   /* a title inside a bar */
  --fsBody:   0.72em;   /* running text in a panel */
  --fsRow:    0.68em;   /* list rows: key/value, entity, run */
  --fsLabel:  0.6em;    /* section heads and labels */
  --fsMicro:  0.52em;   /* the word under a stat */

  /* ── shape ─────────────────────────────────────────────────────────────── */
  --radius:      0.55em;
  --radiusSm:    0.3em;
  /* Tags and chips are SOFTENED RECTANGLES, not capsules — a fully round pill
     reads as a different family from the buttons everything else uses. */
  --radiusChip:  0.28em;
  --mono:        ui-monospace, SFMono-Regular, Consolas, monospace;

  /* ── icons ─────────────────────────────────────────────────────────────── */
  /* Drawn as masks, not images: the shape is here, the colour comes from
     whatever uses it, and there is no file to ship or fail to load. */
  --iconSearch: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><g fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'><circle cx='7' cy='7' r='4.4'/><path d='M10.4 10.4 L14 14'/></g></svg>");
}

/*── .list — WARP'S CLASS. THIS IS A STAND-IN, DELETE ON MERGE ─────────────────
  Warp defines `.list`: at the root of a window it fills whatever height the
  header and footer leave. That is the layout every one of these apps uses —

      <div class="head">…</div>     sizes to its content
      <div class="list">…</div>     takes the rest, scrolls
      <div class="foot">…</div>     sizes to its content

  and it is why nothing else in this file is allowed to be called `.list`
  (see `.crawled > .rows`).

  The four panels — crawler, extraction, classification, claude — are the same
  app four times over: a state header, a list of what it produced, a control
  footer. Written once here, they differ only in what goes in the middle.

  These two rules exist so the components can be shown outside Warp. When the
  stylesheets are joined, delete this block: Warp's definition is the real one.
  `min-height: 0` is the load-bearing half — without it a flex child refuses to
  shrink below its content and scrolls the whole window instead of itself.     */
.list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--lineStrong) transparent;
}
/* the app root: only what the layout above needs, no chrome — that is the
   window's, and the window is Warp's */
.app { display: flex; flex-direction: column; height: 100%; min-height: 0; }

/*── .search ───────────────────────────────────────────────────────────────────
  A filter over whatever the list below is showing — it belongs in the `.head`,
  because it acts on the `.list`, not on the app.

  IT DOES NOT PAINT THE FIELD. No background, border or radius on the input:
  those are Warp's, and this file has no business having a second opinion about
  what an input looks like. What is here is only what Warp cannot know — that
  the field shares a row with a glyph and a count, that it takes the leftover
  width, and that its text is a row, not a heading.

  The magnifier is a MASKED SVG, not a character: ⌕ (U+2315) renders as a weak
  squiggle in most UI faces and is missing from some, and an icon font is an
  asset to ship and load. A mask keeps it crisp at any size and lets it take its
  colour from `background`, so it still follows the text it sits beside.       */
.search { display: flex; align-items: center; gap: 0.45em; font-size: var(--fsRow); }
.search::before {
  content: "";
  flex: 0 0 auto;
  width: 1.05em;
  height: 1.05em;
  background: var(--textFaint);
  -webkit-mask: var(--iconSearch) center / contain no-repeat;
          mask: var(--iconSearch) center / contain no-repeat;
}
.search > input {
  flex: 1 1 auto;
  min-width: 0;                 /* or a long value stretches the whole head */
  font: inherit;                /* the panel's face and size, not the OS default */
}
.search > input::placeholder { color: var(--textGhost); }
/* how many rows survived the filter — "12 / 591" */
.search > .count { flex: 0 0 auto; color: var(--textFaint); font-variant-numeric: tabular-nums; }
.search:focus-within::before { background: var(--accent); }

/*── .led ──────────────────────────────────────────────────────────────────────
  A lamp. Colour and state are separate classes so markup only ever toggles the
  state: `.led.green.on` <-> `.led.green.off`. The glow IS the on state — size
  and hue stay put, so an eye tracking the panel does not have to re-read it.  */
.led {
  display: inline-block;
  flex: 0 0 auto;
  width: 0.6em;
  height: 0.6em;
  border-radius: 50%;
  background: var(--ledOff);
}
.led.big   { width: 0.9em; height: 0.9em; }
.led.small { width: 0.45em; height: 0.45em; }

.led.green.on  { background: var(--ok);     box-shadow: 0 0 0.55em var(--ok); }
.led.green.off { background: var(--ledOffGreen); }
.led.red.on    { background: var(--danger); box-shadow: 0 0 0.55em var(--danger); }
.led.red.off   { background: var(--ledOffRed); }
.led.amber.on  { background: var(--warn);   box-shadow: 0 0 0.55em var(--warn); }
.led.amber.off { background: var(--ledOffAmber); }
.led.blue.on   { background: var(--accent); box-shadow: 0 0 0.55em var(--accent); }
.led.blue.off  { background: var(--ledOffBlue); }

/* The word beside a lamp — ON / OFF / PARKED. It takes its colour FROM the lamp
   through a sibling selector, so the state is set in one place: change the led's
   classes and the word follows. It must therefore come after the led, inside the
   same parent (that is what .ledRow is for). */
.ledState { font-size: 1.05em; font-weight: 700; letter-spacing: 0.07em; }
.led.green.on  ~ .ledState { color: var(--ok); }
.led.red.on    ~ .ledState { color: var(--danger); }
.led.amber.on  ~ .ledState,
.led.amber.off ~ .ledState { color: var(--warn); }
.led.off ~ .ledState { color: var(--textMuted); }
/* the line that carries a lamp, its word, and a quiet note after it */
.ledRow { display: flex; align-items: center; gap: 0.55em; }
.ledRow > .note { font-size: var(--fsRow); color: var(--textMuted); }

/*── .progress > .bar ──────────────────────────────────────────────────────────
  A track and a fill. The fill is a child rather than a background gradient so
  its width is the only thing that ever changes — and so it can be coloured by
  state without touching the track.
  Track height is in px on purpose: it is a rule, not type, and a 4px line at
  every size is the point.                                                     */
.progress {
  height: 4px;
  margin-top: 0.35em;        /* it always follows its own label row; override to 0 if not */
  border-radius: 2px;
  background: var(--bgLighter);
  overflow: hidden;
}
.progress > .bar {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 2px;
  background: var(--barBlue);
  transition: width 0.35s ease;
}
.progress.tall            { height: 7px; border-radius: 3.5px; }
.progress.ok     > .bar   { background: var(--ok); }
.progress.warn   > .bar   { background: var(--warn); }
.progress.danger > .bar   { background: var(--danger); }
/* the caption under a bar: "resets in 1h 38m" */
.progress + .note { display: block; margin-top: 0.25em; font-size: var(--fsMicro); color: var(--textFaint); }

/*── .separator ────────────────────────────────────────────────────────────────
  A label with a rule running off to the right. The rule is a pseudo-element, so
  the label stays hard left and the line always fills exactly what is left over,
  whatever the text.
  `.sticky` pins it while its own section scrolls under it — it paints the page
  background because a sticky element does not paint the gap above itself, and a
  card scrolling through that gap would show through.                          */
.separator {
  display: flex;
  align-items: center;
  gap: 0.55em;
  margin: 0 0 0.5em;
  font-size: var(--fsLabel);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--textFaint);
}
.separator::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.separator > .count { color: var(--textGhost); letter-spacing: 0.1em; }

.separator.big {
  font-size: 1.36em;
  font-weight: 600;
  letter-spacing: 0.12em;
  color: var(--textDim);
}
.separator.big > .count { font-size: 0.62em; font-weight: 400; }

.separator.sticky {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--bgPage);
  margin: 0 0 0.3em;
  padding: 0.9em 0 0.35em;
}

/*── .keyValue ─────────────────────────────────────────────────────────────────
  A stack of label-on-the-left, value-on-the-right rows — the shape most of
  these panels are made of. The value is the loud half; the label is furniture.
  Rows are baseline-aligned so a big value and a small label still sit on the
  same line.                                                                    */
.keyValue { display: flex; flex-direction: column; gap: 0.4em; min-width: 0; font-size: var(--fsRow); }
.keyValue > .row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6em;
}
.keyValue .k { color: var(--textMuted); min-width: 0; }
.keyValue .v { color: var(--textDim); font-weight: 600; text-align: right; white-space: nowrap; }
.keyValue .v.accent { color: var(--accent); }
.keyValue .v.ok     { color: var(--ok); }
.keyValue .v.warn   { color: var(--warn); }
.keyValue .v.danger { color: var(--danger); }
.keyValue > .note   { font-size: 0.85em; color: var(--textFaint); }

/*── .stat / .section ──────────────────────────────────────────────────────────
  One number with a word under it. Shared by the provider card's bottom row and
  the header's section strip, because they are the same object twice — and if
  they ever drift apart, they should drift on purpose, not by copy.
  `flex: 1 1 0` (not `flex: 1`) keeps cells EVEN whatever the digits: a zero-
  basis means the content length stops deciding the width.                      */
.stat,
.section {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  padding: 0.6em 0.15em;
  border-right: 1px solid var(--line);
}
.stat:last-child,
.section:last-child { border-right: none; }

.stat > .n, .section > .n { font-size: var(--fsBig); font-weight: 700; line-height: 1; color: var(--text); }
.stat > .l, .section > .l {
  margin-top: 0.4em;
  font-size: var(--fsMicro);
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--textMuted);
}
/* a stat with something behind it is a button; a zero is not */
.stat.clickable, .section.clickable { cursor: pointer; }
.stat.clickable:hover, .section.clickable:hover { background: var(--bgHover); }
.stat.clickable:hover > .l, .section.clickable:hover > .l { color: var(--textDim); }

.stat.done > .n,   .section.done > .n   { color: var(--accent); }
.stat.ok > .n,     .section.ok > .n     { color: var(--ok); }
.stat.warn > .n,   .section.warn > .n   { color: var(--warn); }
.stat.danger > .n, .section.danger > .n { color: var(--danger); }

/*── .provider ─────────────────────────────────────────────────────────────────
  Two rows: who it is, and what we hold of it. The bottom row is a funnel read
  left to right — each count a subset of the one before it — so the cells are
  even and undivided by anything but a hairline.                                */
.provider { display: flex; flex-direction: column; min-width: 0; }

.provider > .head {
  display: flex;
  align-items: center;
  gap: 0.65em;
  min-height: 3.3em;
  padding: 0.4em 0.8em;
}
.provider > .head > img {
  flex: 0 0 auto;
  width: 1.6em;
  height: 1.6em;
  border-radius: 0.33em;
  background: #000;              /* a transparent favicon needs something behind it */
  object-fit: contain;
}
.provider > .head > .meta { min-width: 0; }         /* so the url can ellipsis */
.provider .name { font-weight: 600; line-height: 1.15; color: var(--text); }
.provider .head a {
  display: block;
  font-size: var(--fsBody);
  color: var(--textMuted);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.provider .head a:hover { color: var(--accent); }

.provider > .stats {
  display: flex;
  border-top: 1px solid var(--line);
  background: var(--bgLighter);
}

/* a provider that keeps failing says so on its own card, once */
.provider > .ill {
  border-top: 1px solid #4a2626;
  background: #2a1a1a;
  padding: 0.55em 0.9em;
  font-size: var(--fsLabel);
  line-height: 1.45;
  color: var(--danger);
}
.provider > .ill > i { display: block; margin-top: 0.2em; font-style: normal; color: #8a6a6a; }

/*── .crawled ──────────────────────────────────────────────────────────────────
  A dated list of records: a sticky head that keeps the title and the day
  visible, then rows of "when" and "which file". The row is an <a> in practice,
  hence the explicit text-decoration reset.                                      */
.crawled { display: flex; flex-direction: column; min-width: 0; }

.crawled > .head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 0.55em;
  padding: 0.2em 0 0.35em;
  background: var(--bgPage);
}
.crawled > .head { align-items: baseline; }
.crawled > .head > .title {
  font-size: 1.15em;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--textDim);
}
/* a count is a number, not a label: no tracking, and tabular figures so it does
   not shift the heading sideways every time it ticks over */
.crawled > .head > .count {
  font-size: 0.78em;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
  color: var(--textFaint);
}
.crawled > .head > .spacer { flex: 1; }

/* `.rows`, not `.list`: Warp already owns `.list` — the class that fills what a
   window's header and footer leave — and two meanings for one name is a merge
   that cannot be undone. Inside a Warp app the rows go straight into Warp's
   `.list`; `.rows` is for the same list living anywhere else. */
.crawled > .rows { display: flex; flex-direction: column; min-width: 0; }
.crawled .row {
  font-size: var(--fsRow);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6em;
  padding: 0.5em 0.75em;
  border-bottom: 1px solid var(--lineSoft);
  color: var(--textDim);
  text-decoration: none;
}
.crawled .row:last-child { border-bottom: none; }
.crawled .row:hover      { background: var(--bgHoverSoft); color: var(--text); }
.crawled .row.current    { background: var(--bgHoverSoft); }
.crawled .row > .file    { font-size: 0.85em; color: var(--textFaint); }
.crawled .empty          { padding: 0.8em 0.75em; color: var(--textGhost); }

/*── .header > .sections ───────────────────────────────────────────────────────
  A title bar with what it is, what it points at, and its actions — then a strip
  of numbers under it. Two rows rather than one because the numbers describe the
  thing named above them, and a single row of eight items describes nothing.    */
.header { display: flex; flex-direction: column; min-width: 0; }

.header > .bar { display: flex; align-items: center; gap: 0.65em; padding: 0.7em 0.9em; }
.header > .bar > .title { flex: 0 0 auto; font-size: var(--fsTitle); font-weight: 600; color: var(--text); }
.header > .bar > .url {
  flex: 1;
  min-width: 0;
  font-size: 0.8em;
  color: var(--textMuted);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.header > .bar > .url:hover { color: var(--accent); }

.header > .sections {
  display: flex;
  border-top: 1px solid var(--line);
  background: var(--bgLighter);
}
.header > .sections > .section { padding: 0.55em 0.3em; }

/* the line under the strip: when it was made, which contract */
.header > .note { padding: 0.5em 0.9em; font-size: var(--fsBody); color: var(--textFaint); }

/*── .entity ───────────────────────────────────────────────────────────────────
  One extracted thing: its local id, its name, what kind it is, how sure we are,
  a sentence of identity and the words on the page that produced it.

  KIND COLOURS THE NAME AND ITS TAG, and nothing else. The description stays one
  colour for every kind: the title already says what this is, and a second
  channel carrying the same meaning is noise. The evidence is quoted rather than
  stated — it is what the page said, not what we concluded.                     */
.entity { min-width: 0; font-size: var(--fsRow); }

.entity > .top { display: flex; align-items: center; gap: 0.45em; flex-wrap: wrap; }
.entity .id   { flex: 0 0 auto; font-family: var(--mono); font-size: 0.72em; color: var(--textGhost); }
.entity .name { font-weight: 600; color: var(--text); }
.entity .tag  {
  font-size: 0.75em;
  padding: 0.1em 0.5em;
  border: 1px solid var(--line);
  border-radius: var(--radiusChip);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--textMuted);
  white-space: nowrap;
}
.entity .conf { margin-left: auto; font-size: 0.82em; color: var(--textMuted); }
.entity .desc { margin-top: 0.25em; color: var(--textDim); }
.entity .evidence {
  margin-top: 0.3em;
  padding-left: 0.5em;
  border-left: 2px solid var(--line);
  font-size: 0.85em;
  font-style: italic;
  color: var(--textFaint);
}

.entity.person .name, .entity.person .tag { color: var(--person); }
.entity.group  .name, .entity.group  .tag { color: var(--group); }
.entity.place  .name, .entity.place  .tag { color: var(--place); }
.entity.event  .name, .entity.event  .tag { color: var(--event); }
.entity.work   .name, .entity.work   .tag { color: var(--work); }
.entity.fact   .name, .entity.fact   .tag { color: var(--fact); }
/* the tag border picks up its kind faintly — a tint, not an outline */
.entity.person .tag { border-color: color-mix(in srgb, var(--person) 35%, transparent); }
.entity.group  .tag { border-color: color-mix(in srgb, var(--group)  35%, transparent); }
.entity.place  .tag { border-color: color-mix(in srgb, var(--place)  35%, transparent); }
.entity.event  .tag { border-color: color-mix(in srgb, var(--event)  35%, transparent); }
.entity.work   .tag { border-color: color-mix(in srgb, var(--work)   35%, transparent); }
.entity.fact   .tag { border-color: color-mix(in srgb, var(--fact)   35%, transparent); }

/*── .graph-bubble ─────────────────────────────────────────────────────────────
  The same entity as a NODE in the 3D graph: a floating card the force layout
  positions, with edges running to it.

  This one DOES carry its own frame — background, border, radius, shadow — and
  that is not a contradiction of the no-panel rule above. It is not sitting in a
  panel; it is sitting in space over a dark canvas, and without a surface of its
  own the text would be unreadable the moment an edge passed behind it. The
  shadow is what lifts it off the lines.

  It reuses .entity's kind classes for colour — `.graph-bubble.person` — so a
  node and its row in the list are visibly the same thing.

  POINTER EVENTS ARE OFF on the card and back ON for `.more`: a 215px card that
  swallowed clicks would kill orbit-dragging over most of the canvas, so only
  the expand chip is a target.                                                  */
.graph-bubble {
  width: 215px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: color-mix(in srgb, var(--panelBg) 94%, transparent);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.55);
  font-size: 13px;
  text-align: left;
  pointer-events: none;
  user-select: none;
}
/* A portrait, when the entity has one (entity.schema.json's image_url). The face
   is the fastest identifier there is — on a canvas of forty cards it beats
   reading forty names. Laid out with :has() so the markup stays the same whether
   there is an image or not; where :has() is missing the picture simply stacks
   above the text, which is still readable. */
.graph-bubble:has(> .portrait) {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  /* The chip goes UNDER THE PICTURE, not under the text: the left column is
     already exactly the width of the thumbnail, so a centred chip there lines
     up with it and the description keeps its full measure instead of being
     interrupted by a button. */
  grid-template-areas: "pic top" "pic desc" "more desc" "sel desc";
  column-gap: 8px;
  align-items: start;
}
.graph-bubble > .portrait {
  grid-area: pic;
  /* a third of the card. At 44px a face was an icon you had to lean into; at
     this size it identifies the entity from across the canvas, which is the
     only reason the picture is on the small card at all. */
  width: 70px;
  height: 70px;
  border-radius: 4px;
  object-fit: cover;
  object-position: top center;      /* a head is at the top of a photo, not its middle */
  background: #000;                 /* something behind a transparent or slow image */
}
.graph-bubble > .top  { grid-area: top; }
.graph-bubble > .desc { grid-area: desc; }
/* centred on the thumbnail above it, and its own margin rather than the grid's
   row gap — 10px is the distance from the picture, not from whatever else might
   end up in that column later */
.graph-bubble > .more   { grid-area: more; justify-self: center; margin-top: 10px; }
/* directly under the expander, in the column the picture already sizes */
.graph-bubble > .selall { grid-area: sel;  justify-self: center; margin-top: 5px; }

.graph-bubble > .top  { display: flex; align-items: baseline; gap: 5px; flex-wrap: wrap; }
.graph-bubble .name   { font-weight: 700; font-size: 1em; line-height: 1.25; word-break: break-word; color: var(--text); }
.graph-bubble .tag    {
  padding: 0 5px;
  border: 1px solid var(--lineStrong);
  border-radius: var(--radiusChip);
  font-size: 0.73em;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--textMuted);
}
/* one colour for every kind's body text — the title already carries the kind */
.graph-bubble > .desc {
  display: -webkit-box;
  -webkit-line-clamp: 3;                /* three lines, then stop: these are nodes, not articles */
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 4px;
  font-size: 0.88em;
  line-height: 1.35;
  color: var(--textDim);
}
/* the expand chip: "+3" collapsed, "−" open. The only clickable part. */
.graph-bubble > .more {
  display: inline-block;
  margin-top: 6px;
  padding: 1px 8px;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: var(--radiusChip);
  background: var(--bgDarkest);
  font-size: 0.8em;
  font-weight: 700;
  color: var(--accent);
  cursor: pointer;
  pointer-events: auto;
}
.graph-bubble > .more:hover { background: var(--accent); border-color: var(--accent); color: #101010; }
/* Select all — the same chip, quieter, because it acts on what is already on
   screen rather than asking the server for anything. */
.graph-bubble > .selall {
  display: inline-block;
  padding: 1px 8px;
  border: 1px solid color-mix(in srgb, var(--textDim) 40%, transparent);
  border-radius: var(--radiusChip);
  background: var(--bgDarkest);
  font-size: 0.78em;
  font-weight: 600;
  color: var(--textDim);
  white-space: nowrap;
  cursor: pointer;
  pointer-events: auto;
}
.graph-bubble > .selall:hover { border-color: var(--ok, #5fbf6a); color: var(--ok, #5fbf6a); }
/* nothing to select: this card is hiding its children, or has none */
.graph-bubble > .selall.off { opacity: 0.5; cursor: default; pointer-events: none; }
.graph-bubble > .selall.off:hover { border-color: color-mix(in srgb, var(--textDim) 40%, transparent); color: var(--textDim); }
/* Nothing behind this one — every connection it has is already on screen. Shown
   and dead rather than removed: a button that is missing on some cards and not
   others reads as a bug, while a dim one reads as an answer. */
.graph-bubble > .more.off {
  /* Faded as a whole rather than recoloured piece by piece: the border, the
     background and the text all have to read as "not for pressing", and one
     opacity says that in a way three separate colours did not. */
  opacity: 0.3;
  cursor: default;
  pointer-events: none;
}
.graph-bubble > .more.off:hover { opacity: 0.3; background: var(--bgDarkest); }

.graph-bubble.person .name, .graph-bubble.person .tag { color: var(--person); }
.graph-bubble.group  .name, .graph-bubble.group  .tag { color: var(--group); }
.graph-bubble.place  .name, .graph-bubble.place  .tag { color: var(--place); }
.graph-bubble.event  .name, .graph-bubble.event  .tag { color: var(--event); }
.graph-bubble.work   .name, .graph-bubble.work   .tag { color: var(--work); }
.graph-bubble.fact   .name, .graph-bubble.fact   .tag { color: var(--fact); }
/* the page node itself — the document every entity was pulled out of */
.graph-bubble.page   .name { color: #fff; }

/*── .graph-bubble.big ─────────────────────────────────────────────────────────
  The same node, opened up: the one you clicked, or the one the graph is about.
  Everything the small card had to leave out is here — the portrait at full
  width, the description unclipped to five lines, and the entity's own facts and
  affiliations listed under a rule.

  The portrait goes from a 44px thumbnail to 220px — five times the size, and
  the reason for the whole variant: at thumbnail size a face is an identifier,
  at this size it is a photograph. So the layout stops being a grid with a
  picture beside the text and becomes a stack with a picture on top of it.     */
.graph-bubble.big {
  width: 320px;
  padding: 0 0 10px;                  /* zero at the top: the image runs to the edges */
  font-size: 14px;
}
.graph-bubble.big:has(> .portrait) { display: block; }   /* undo the thumbnail grid */

.graph-bubble.big > .portrait {
  width: 100%;
  height: 220px;
  /* CONTAIN, not cover. Cover fills the frame by cutting, and what it cuts is
     the top of a head or the side of a logo — the two things the picture is
     there to show. Letterboxing against the well colour costs nothing; a
     beheaded portrait costs the identification. */
  object-fit: contain;
  object-position: center;
  background: var(--bgDarkest);
  border-radius: 7px 7px 0 0;         /* 1px inside the card's own 8px, or it corners badly */
  margin-bottom: 9px;
  display: block;
}
.graph-bubble.big > .top,
.graph-bubble.big > .desc,
.graph-bubble.big > .items,
.graph-bubble.big > .more { margin-left: 11px; margin-right: 11px; }

.graph-bubble.big .name { font-size: 1.15em; }
.graph-bubble.big > .desc { -webkit-line-clamp: 5; margin-top: 5px; }

/* the entity's own facts and affiliations — predicate on the left, value on the
   right, the same reading order as the extraction they came from */
.graph-bubble > .items {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 9px;
  padding-top: 7px;
  border-top: 1px solid var(--line);
}
.graph-bubble .item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 9px;
  font-size: 0.85em;
}
.graph-bubble .item > .k {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 0.92em;
  color: var(--textFaint);
}
.graph-bubble .item > .v {
  min-width: 0;
  text-align: right;
  font-weight: 600;
  color: var(--textDim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* an affiliation that is another entity is a link into the graph, not a string */
.graph-bubble .item > .v.node { color: var(--accent); cursor: pointer; pointer-events: auto; }
.graph-bubble .item > .v.node:hover { text-decoration: underline; }

/* the label riding an edge between two bubbles */
.graph-edge-label {
  padding: 0 3px;
  border-radius: 3px;
  background: rgba(14, 14, 14, 0.6);
  font-family: var(--mono);
  font-size: 10.5px;
  color: #9fb8d0;
  pointer-events: none;
  user-select: none;
}
