

/* ========================================================================
   DESIGN TOKENS
   The only place a raw value is allowed. Rules below refer to these by name,
   so a change to the scale propagates rather than needing a search.
   ===================================================================== */
:root{
  
  --colour-ink:#0f0f0f;
  --colour-ink-muted:#5a5a5a;
  --colour-ink-faint:#8a8a8a;
  --colour-rule:#e6e6e6;
  --colour-rule-soft:#f2f2f2;
  --colour-page:#fff;
  
  --colour-highlight-new:#e8f0f9;   
  --colour-highlight-open:#f2f2f2;  
  --colour-surface-sunken:#fafafa;

  /* How long one breath of a working run card takes, end to end. The trade is
     legibility against nagging: too fast reads as an alarm, too slow is
     indistinguishable from a still card. One place to change it. */
  --runs-breathe-period:1.5s;

  /* One tint per workflow (A-E). Equal saturation and lightness so none ranks
     above another; the letter identifies the arm, the tint only groups a column.
     Searched, not picked: pairs >=25 apart in RGB, >=15 from the four grounds a
     tag sits on, ink 13.8:1. */
  --colour-arm-a:#f2d4d4;
  --colour-arm-b:#ecf2d4;
  --colour-arm-c:#d4f2e0;
  --colour-arm-d:#d4e0f2;
  --colour-arm-e:#ecd4f2;
  --colour-alert:#8a2b2b;

  --family-display:"Lyon Text",Charter,"Iowan Old Style",Georgia,serif;
  --family-text:"Neue Haas Grotesk","Helvetica Neue",Helvetica,Arial,sans-serif;
  --family-code:ui-monospace,SFMono-Regular,Menlo,monospace;

  --size-caption:11px;   
  --size-detail:13px;    
  --size-body:15px;      
  --size-title:18px;     
  --size-display:32px;   

  --leading-tight:1.2;
  --leading-normal:1.6;
  --leading-loose:1.7;
  --tracking-caption:.08em;

  --space-hair:4px;
  --space-tight:8px;
  --space-snug:12px;
  --space-base:16px;
  --space-comfy:22px;
  --space-loose:32px;
  --space-section:44px;

  
  --gutter:30px;
  --column-search:400px;
  --measure-prose:660px;      
  --measure-note:620px;
  --masthead-height:61px;
}

/* ========================================================================
   DOCUMENT DEFAULTS
   Tag selectors appear here and nowhere else.
   ===================================================================== */
*{box-sizing:border-box}

html{background:var(--colour-page);-webkit-text-size-adjust:100%}

body{
  margin:0;
  background:var(--colour-page);
  color:var(--colour-ink);
  font-family:var(--family-text);
  font-size:var(--size-body);
  line-height:var(--leading-normal);
  -webkit-font-smoothing:antialiased;
}

img{max-width:100%}

[x-cloak]{display:none!important}

/* ========================================================================
   BLOCK: masthead
   ===================================================================== */

.masthead{
  display:grid;
  grid-template-columns:auto 1fr auto;
  align-items:center;
  gap:var(--space-loose);
  position:sticky;
  top:0;
  z-index:40;
  background:var(--colour-page);
  padding:20px 34px;
  border-bottom:1px solid var(--colour-rule);
}

.masthead__nav{
  justify-self:start;
  display:flex;
  gap:var(--space-base);
  align-items:baseline;
}

.masthead__institution{
  justify-self:end;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
}

@media (max-width:900px){
  .masthead{padding:var(--space-base) 20px}
}

/* ========================================================================
   BLOCK: workspace — the two columns
   Each column scrolls independently: the shell is pinned to the viewport so
   that reading a long story never scrolls the encounter list away, and vice
   versa. Below 900px the columns stack and the page scrolls as one.
   ===================================================================== */
.workspace{
  display:grid;
  grid-template-columns:var(--column-search) 1fr;
  height:calc(100vh - var(--masthead-height));
  overflow:hidden;
}

.workspace__search,
.workspace__reading{
  overflow-y:auto;
  overscroll-behavior:contain;   
}

.workspace__reading{padding:40px 48px 100px}

/* NO side padding on this column. It is a scroll container, and a scroll container clips its content at the padding edge - which is exactly what cut the highlighted row's bleed off at the scrollbar gutter on the right while the left bled fine. */
.workspace__search{
  border-right:1px solid var(--colour-rule);
  padding-block:24px 80px;
}

@media (max-width:900px){
  .workspace{grid-template-columns:1fr;height:auto;overflow:visible}
  .workspace__search{
    border-right:0;
    border-bottom:1px solid var(--colour-rule);
    
    --gutter:20px;
    padding-block:var(--space-comfy) 40px;
    overflow-y:visible;
  }
  .workspace__reading{padding:28px 20px 70px;overflow-y:visible}
}

/* A top-level section: a rule above it, then its own breathing room below that
   rule. The MARGIN is gone - every container that stacks these now supplies a
   gap, and a margin on top of that gap double-spaced them. The padding stays:
   that is the space between the rule and the section's own content, which
   belongs to the section however it is stacked. */
.section{
  border-top:1px solid var(--colour-rule);
  padding:var(--space-comfy) 0 0;
  margin:0;
}

/* ========================================================================
   SHARED BLOCKS — used by BOTH pages.
   Moved here from encounters.css when the objects page was added: each of
   these renders the same thing on both sides (a search box, a row of
   photographs, a stored record opening into its fields, a raw JSON dump),
   so duplicating them would guarantee the two pages drift apart.
   ===================================================================== */

/* ========================================================================
   BLOCK: search-form
   Full width, so its closing rule spans the column edge to edge; the gutter is
   applied as padding here, keeping the fields aligned with the list below.
   ===================================================================== */

.search-form{
  display:grid;
  gap:var(--space-snug);
  margin:0;
  padding:0 var(--gutter) var(--space-comfy);
  border-bottom:1px solid var(--colour-rule);
}

.search-form__heading{
  margin:0;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:700;
}

.search-form__field{margin:0}

.search-form__input{
  width:100%;
  font:inherit;
  font-size:var(--size-body);
  padding:var(--space-tight) var(--space-snug);
  border:1px solid var(--colour-rule);
  border-radius:0;
  background:var(--colour-page);
  color:var(--colour-ink);
}

.search-form__input:focus{outline:2px solid var(--colour-ink);outline-offset:-2px}

.search-form__submit{
  width:100%;
  font:inherit;
  font-size:var(--size-body);
  padding:10px var(--space-comfy);
  margin:0;
  border:1px solid var(--colour-ink);
  border-radius:0;
  background:var(--colour-ink);
  color:var(--colour-page);
  cursor:pointer;
}

.search-form__submit:disabled{opacity:.4;cursor:default}

@media (max-width:900px){
}

.plate-row{
  display:flex;
  flex-wrap:wrap;
  align-items:flex-end;
  gap:var(--space-comfy);
  margin:0;
}

.plate{margin:0;flex:0 1 auto;max-width:300px}
.plate__image{display:block;max-height:280px;width:auto}

.plate--large{max-width:600px}

/* `width:auto` with no `height`, so an image SMALLER than the cap renders at its own size rather than being stretched: originals run 282-1200px on the long edge and 252 of the 13,815 are shorter than this cap, which would otherwise upscale them into softness. max-* is a ceiling, never a target. */
.plate--large .plate__image{max-height:560px;max-width:100%}

.plate__caption{
  margin:10px 0 0;
  font-size:var(--size-caption);
  line-height:1.5;
}

.plate__accession{display:block;color:var(--colour-ink-faint)}
.plate__accession--seed{color:var(--colour-ink);font-weight:600}
.plate__title{display:block}
.plate__facets{display:block;color:var(--colour-ink-faint)}

.unexpectedness__heading{
  display:block;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  margin:0 0 var(--space-tight);
}

.unexpectedness__body{
  font-size:var(--size-detail);
  color:var(--colour-ink-muted);
  margin:0;
}

.field-list{margin:0 0 10px;font-size:var(--size-detail)}

.field-list__row{
  display:grid;
  grid-template-columns:170px minmax(0,1fr);
  gap:14px;
  padding:6px 0;
  border-bottom:1px solid var(--colour-rule-soft);
}

.field-list__row:last-child{border-bottom:0}

.field-list__key{
  font-family:var(--family-code);
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  word-break:break-word;
}

.field-list__value{margin:0;line-height:1.55;word-break:break-word}

.field-list__value pre{
  margin:0;
  font-family:var(--family-code);
  font-size:var(--size-caption);
  line-height:1.5;
  white-space:pre-wrap;
  word-break:break-word;
  color:var(--colour-ink-muted);
}

@media (max-width:700px){
  .field-list__row{grid-template-columns:1fr;gap:2px}
}

.masthead__link{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  text-decoration:none;
}

a.masthead__link:hover{color:var(--colour-ink);text-decoration:underline}
.masthead__link:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

/* The page you are on. The SAME underline as the hover state above - a real text-decoration, not a border-bottom: a border sits at the bottom of the box rather than on the text baseline, so at this size the two marks landed a few pixels apart and at different widths, and moving between them looked like the rule was jumping. */
.masthead__link--here{
  color:var(--colour-ink);
  text-decoration:underline;
}

.plate--link{
  display:block;
  color:inherit;
  text-decoration:none;
}

.plate--link:hover .plate__title{text-decoration:underline}
.plate--link:hover .plate__image{opacity:.88}
.plate--link:focus-visible{outline:2px solid var(--colour-ink);outline-offset:3px}

.search-form__submit:focus-visible{
  outline:2px solid var(--colour-ink);
  outline-offset:2px;
}

@media (max-width:700px){
  
  .masthead{padding:var(--space-snug) 20px}
  .masthead__nav{gap:var(--space-snug)}
  .masthead__institution{font-size:10px}
}

a.masthead__institution{text-decoration:none;color:var(--colour-ink-faint)}
a.masthead__institution:hover{color:var(--colour-ink);text-decoration:underline}
a.masthead__institution:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

.visually-hidden{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;
  white-space:nowrap;
}

/* ======================================================================
   PAGE: index — the doorway
   ================================================================== */

.doorway{
  display:grid;
  gap:var(--space-section);
  max-width:980px;
  margin:0 auto;
  padding:var(--space-section) var(--gutter) 100px;
}

.doorway__intro{
  max-width:var(--measure-prose);
  margin:0;
}

.doorway__title{
  font-family:var(--family-display);
  font-size:var(--size-display);
  line-height:var(--leading-tight);
  font-weight:500;
  margin:0 0 var(--space-base);
}

.doorway__standfirst{
  font-size:var(--size-body);
  line-height:var(--leading-loose);
  color:var(--colour-ink-muted);
  margin:0;
}

/* ========================================================================
   BLOCK: doors — the two ways in
   Equal columns on purpose: neither page is the "main" one. They collapse to
   one column before the text gets cramped rather than at a device width.
   ===================================================================== */
.doors{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));
  gap:var(--space-comfy);
}

.door{
  display:grid;
  align-content:start;
  gap:var(--space-tight);
  padding:var(--space-comfy);
  border:1px solid var(--colour-rule);
  color:inherit;
  text-decoration:none;
  background:var(--colour-page);
}

.door:hover .door__name{text-decoration:underline}
.door:focus-visible{outline:2px solid var(--colour-ink);outline-offset:3px}

.door__eyebrow{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
}

.door__name{
  font-family:var(--family-display);
  font-size:var(--size-display);
  line-height:var(--leading-tight);
  font-weight:500;
}

.door__blurb{
  font-size:var(--size-detail);
  line-height:var(--leading-loose);
  color:var(--colour-ink-muted);
  margin:0;
}

.door__facts{
  display:flex;
  flex-wrap:wrap;
  gap:var(--space-hair) var(--space-base);
  padding:var(--space-tight) 0;
  border-top:1px solid var(--colour-rule-soft);
}

.door__fact{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
}

.door__fact b{
  font-family:var(--family-code);
  font-size:var(--size-detail);
  letter-spacing:0;
  color:var(--colour-ink);
  font-weight:400;
  margin-right:3px;
}

.door__sample{
  display:grid;
  grid-template-columns:56px minmax(0, 1fr);
  gap:var(--space-snug);
  align-items:center;
  padding-top:var(--space-tight);
  border-top:1px solid var(--colour-rule-soft);
}

/* The encounters door has no picture, so its sample is text only and must not leave a 56px hole where an image would have been. */
.door__sample--text{grid-template-columns:minmax(0, 1fr)}

.door__sample-image{
  width:56px;
  height:56px;
  object-fit:cover;
  background:var(--colour-surface-sunken);
  border:1px solid var(--colour-rule);
}

.door__sample-text{
  display:grid;
  gap:1px;
  min-width:0;
}

.door__sample-label{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
}

.door__sample-title{
  font-family:var(--family-display);
  font-size:var(--size-detail);
  line-height:1.3;
  color:var(--colour-ink-muted);
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

.doorway__status{
  margin:0;
  max-width:var(--measure-note);
  font-size:var(--size-detail);
  color:var(--colour-ink-faint);
}

.doorway__status--error{
  color:var(--colour-alert);
  border-left:3px solid var(--colour-alert);
  padding-left:var(--space-snug);
}

@media (max-width:900px){
  .doorway{padding:var(--space-comfy) 20px 70px}
  .doorway__title{font-size:26px}
  .door__name{font-size:26px}
}

/* ======================================================================
   PAGE: encounters
   ================================================================== */

/* ========================================================================
   BLOCK: encounter-list
   Separators are drawn as border-TOP on each entry rather than border-bottom,
   which is what lets a highlighted entry suppress the rule above it as well
   as the one below (`.entry + .entry`); with border-bottom the rule above
   belongs to the previous sibling and no selector can reach it.
   ===================================================================== */

/* A grid of full-width rows. The column itself no longer carries side padding (see .workspace__search) - each child pads itself instead - so a row that should bleed simply drops its own padding-inline to 0 and already touches both edges. */
.encounter-list{
  display:grid;
  grid-template-columns:minmax(0, 1fr);
}

/* Shown only when the live feed cannot reach n8n. It used to sit inside the list heading; with that heading gone it is a row of its own, so it needs its own gutter and spacing rather than a left margin. */
.encounter-list__feed-state{
  font-size:var(--size-detail);
  color:var(--colour-alert);
  padding:var(--space-snug) var(--gutter);
  margin:0;
}

.encounter-list__empty{
  font-size:var(--size-body);
  color:var(--colour-ink-faint);
  padding:24px var(--gutter);
}

.encounter-list__entry{
  display:block;
  width:100%;
  text-align:start;
  font:inherit;
  padding:15px var(--gutter);
  border:0;
  border-top:1px solid var(--colour-rule);
  background:none;
  cursor:pointer;
}

.encounter-list__entry:focus-visible{outline:2px solid var(--colour-ink);outline-offset:-2px}
.encounter-list__entry:hover .encounter-list__title{text-decoration:underline}

/* A highlighted entry fills the column edge to edge. It needs no bleed at all any more: the column carries no side padding, so a row is already the full width and its background reaches both edges on its own. */
.encounter-list__entry--new,

.encounter-list__entry--new + .encounter-list__entry,
.encounter-list__entry--open + .encounter-list__entry{border-top-color:transparent}

.encounter-list__entry--open{
  background:var(--colour-highlight-open);
  border-top-color:transparent;
}
.encounter-list__entry--new,
.encounter-list__entry--new.encounter-list__entry--open{background:var(--colour-highlight-new)}

.encounter-list__title{
  font-family:var(--family-display);
  font-size:var(--size-title);
  line-height:1.25;
  margin:0 0 5px;
}

.encounter-list__excerpt{
  font-size:var(--size-detail);
  color:var(--colour-ink-muted);
  margin:0 0 7px;
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

.encounter-list__meta{
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  margin:0;
}

/* ========================================================================
   BLOCK: encounter — one encounter, read in full
   ===================================================================== */
.encounter__placeholder{
  font-family:var(--family-display);
  font-size:var(--size-title);
  color:var(--colour-ink-faint);
  max-width:420px;
  padding:80px 0;
}

.encounter__placeholder--error{color:var(--colour-alert)}

/* THE ENCOUNTER BODY. Its blocks - header, photographs, narrative, echo,
   provenance - are stacked by this gap. Each used to carry its own bottom
   margin, which meant the spacing between any two of them depended on which
   one happened to render, and the last one always left a gap under itself. */
.encounter{display:grid;gap:var(--space-loose)}

.encounter__header{max-width:760px;margin:0}

.encounter__title{
  font-family:var(--family-display);
  font-size:var(--size-display);
  line-height:1.15;
  font-weight:500;
  text-wrap:balance;
  margin:0 0 14px;
}

.encounter__provenance-line{
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  margin:0;
}

/* WHICH WORKFLOW WROTE THIS, as a one-letter tag on the list row. It was briefly plain text ("D workflow") and that was worse: the word repeated on every row and the letter stopped being scannable down the column. */
.encounter-list__arm{display:inline-block;margin-left:var(--space-tight)}

.encounter__narrative{
  display:grid;
  gap:var(--space-base);
  max-width:var(--measure-prose);
  font-size:var(--size-body);
  line-height:var(--leading-loose);
}

.encounter__paragraph{margin:0}

@media (max-width:900px){
  .encounter__title{font-size:24px}
}

/* ========================================================================
   BLOCK: provenance — the debugging surface
   Deliberately quieter than the story above it: caption-sized type, monospace
   for anything a query would return verbatim, and hairlines instead of boxes,
   so it reads as apparatus rather than as more exhibition.
   ===================================================================== */

.provenance{max-width:820px}

.provenance__heading{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:700;
  margin:0;
  padding:var(--space-base) 0 var(--space-snug);
}

.provenance__status{
  font-size:var(--size-detail);
  color:var(--colour-ink-faint);
  margin:0;
}

.provenance__status--error{color:var(--colour-alert)}

/* The panel holds a loading line, an error line and the levels - at most two of
   them at once. A gap spaces whichever are present without any of them needing
   to know what else rendered. */
.provenance__panel{display:grid;gap:var(--space-base)}

/* The panel's sections are stacked by their PARENT's gap, not by a trailing
   margin on each. A bottom margin on a child is invisible from the container,
   collapses unpredictably against its neighbour, and leaves a stray gap under
   the last one; the gap sits in exactly one place and cannot do either. */
.provenance__levels{display:grid;gap:var(--space-comfy)}

.provenance__section{
  margin:0;
  padding:20px 0 0;
  border-top:1px solid var(--colour-rule);
}

.provenance__section-heading,
.provenance__subheading{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  margin:14px 0 7px;
}

.provenance__section-heading{margin:0 0 10px}
.provenance__note{
  max-width:var(--measure-note);
  font-size:var(--size-detail);
  line-height:1.55;
  color:var(--colour-ink-muted);
  margin:0 0 var(--space-snug);
}

.provenance__note code{font-family:var(--family-code);font-size:var(--size-caption)}

/* ------------------------------------------------------------------------
   THE PROMPTS BEHIND AN ENCOUNTER
   One row per pipeline stage, in execution order; the row opens into the
   immutable prompt text. Spacing is a GAP on the list, never a margin on a
   row - a trailing margin leaves a stray gap under the last stage.
   --------------------------------------------------------------------- */
.prompts__list{
  list-style:none;
  margin:0;
  padding:0;
  display:grid;
  gap:var(--space-hair);
}

.prompts__stage{
  border-top:1px solid var(--colour-rule-soft);
}

/* THE WHOLE ROW IS THE CONTROL. A prompt is what the reader opened this
   section for, so the target is the row rather than a separate marker. */
.prompts__summary{
  display:grid;
  grid-template-columns:auto 1fr auto auto;
  align-items:baseline;
  gap:var(--space-snug);
  width:100%;
  margin:0;
  padding:var(--space-snug) 0;
  border:0;
  background:none;
  font:inherit;
  color:inherit;
  text-align:left;
  cursor:pointer;
}

.prompts__summary:hover .prompts__label{text-decoration:underline}

/* Focus is visible because every disclosure in this app must be reachable by
   keyboard - a UX review found none of them were. */
.prompts__summary:focus-visible{
  outline:2px solid var(--colour-ink);
  outline-offset:2px;
}

/* The step number carries the ORDER, which is the section's whole argument:
   these prompts ran one after another and each constrained the next. */
.prompts__step{
  font-family:var(--family-code);
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  font-variant-numeric:tabular-nums;
}

.prompts__naming{display:block}

.prompts__label{
  display:block;
  font-size:var(--size-detail);
  color:var(--colour-ink);
}

/* What the stage DOES. "Planner" means nothing to a visitor, and a version
   string without it is apparatus rather than an explanation. */
.prompts__blurb{
  display:block;
  font-size:var(--size-caption);
  line-height:1.5;
  color:var(--colour-ink-muted);
}

.prompts__version{
  font-family:var(--family-code);
  font-size:var(--size-caption);
  color:var(--colour-ink-muted);
}

.prompts__size{
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  font-variant-numeric:tabular-nums;
}

/* THE PROMPT AS AN ARTIFACT, not as reflowed prose. Whitespace is preserved
   because a prompt's structure - its headings, its lists, its worked examples -
   is part of what it says. It scrolls inside its OWN container so a 20 KB
   prompt can never make the page scroll sideways. */
.prompts__body{padding:0 0 var(--space-base)}

.prompts__text{
  max-height:420px;
  overflow:auto;
  margin:0;
  padding:var(--space-base);
  background:var(--colour-surface-sunken);
  border:1px solid var(--colour-rule);
  font-family:var(--family-code);
  font-size:var(--size-caption);
  line-height:1.65;
  color:var(--colour-ink);
  white-space:pre-wrap;
  overflow-wrap:anywhere;
}

.prompts__missing{
  margin:0;
  font-size:var(--size-detail);
  color:var(--colour-alert);
}

/* A stage that ran and was not recorded. Stated, never omitted: a short chain
   would otherwise read as a complete short pipeline. */
.prompts__gap{margin:var(--space-snug) 0 0}

.provenance__reason{
  max-width:var(--measure-note);
  font-family:var(--family-display);
  font-size:var(--size-body);
  line-height:1.5;
  color:var(--colour-ink);
  margin:0;
}

.provenance__identifier{font-family:var(--family-code);font-size:var(--size-caption)}

.search-query{display:grid;gap:var(--space-comfy);margin:0}

/* A FIELD LABEL, not a second section heading. It is an <h3> for document
   structure, so the weight MUST be stated: without it the tag default is bold
   and it rendered identically to `.provenance__heading` directly above, giving
   two same-weight headings stacked with nothing between them. A style that
   depends on a tag default is not shared, it is coincidental - the same trap
   the two masthead headings hit. Matches `.run-facts__term`, which is what this
   actually is. */
.search-query__label{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:400;
  margin:0 0 3px;
}

/* Two parts when a topic occasioned the run: the topic, then the claim it
   produced.

   SPACED BY THE PARENT, not by an adjacent-sibling selector. Alpine renders
   `<template x-if>` as a sibling AFTER the template tag, so the two parts are
   never adjacent - a <template> sits between them and `+` silently never
   matches. Measured: margin-top computed to 0px and the second label sat hard
   against the paragraph above it. A grid gap does not care what is interleaved -
   the gap is declared on `.search-query` above. */

/* One part of the query block - the topic, or the claim. It needs no spacing of
   its own: `.search-query` is a grid and its gap separates the parts. Declared
   so the class exists in the stylesheet rather than only in markup. */
.search-query__part{margin:0}

.search-query__why{
  max-width:var(--measure-note);
  font-size:var(--size-detail);
  line-height:1.55;
  color:var(--colour-ink-muted);
  margin:3px 0 0;
}

.search-query__text{
  max-width:var(--measure-note);
  font-family:var(--family-display);
  font-size:var(--size-body);
  line-height:1.5;
  color:var(--colour-ink);
  margin:0;
}

.run-facts{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
  gap:14px 20px;
  margin:0;
}

.run-facts__fact{margin:0}

/* The run id is a full uuid - 36 characters against the 150px minimum every
   other fact is sized for - so it wraps mid-identifier in a single track.
   Spanning two columns gives it a line of its own width. `span 2` is capped
   at the real column count by the grid itself, so at narrow widths, where
   auto-fit collapses to one column, this is a no-op rather than an overflow. */
.run-facts__fact--wide{grid-column:span 2}

.run-facts__term{
  display:block;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:400;
  margin:0 0 3px;
}

.run-facts__value{margin:0;font-size:var(--size-detail);word-break:break-word}

/* THE FUNNEL. Five stages of one narrowing, read top to bottom, bars scaled to
   the widest stage so the SHAPE of the drop carries the meaning rather than the
   absolute width. Every value is a token; nothing here is styled by tag. */
.funnel{
  list-style:none;
  margin:0 0 var(--space-comfy);
  padding:0;
  display:grid;
  gap:var(--space-hair);
}

.funnel__stage{
  display:grid;
  /* label | bar | count | drop. The bar takes the slack so every label and
     count aligns down the column regardless of word length. */
  grid-template-columns:88px 1fr 44px 44px;
  align-items:center;
  gap:var(--space-snug);
}

.funnel__label{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
}

.funnel__bar{
  display:block;
  height:10px;
  background:var(--colour-rule-soft);
  border-radius:2px;
  overflow:hidden;
}

.funnel__fill{
  display:block;
  height:100%;
  background:var(--colour-ink-muted);
  border-radius:2px;
}

.funnel__count{
  font-size:var(--size-detail);
  text-align:right;
  font-variant-numeric:tabular-nums;
}

/* The drop is a loss, so it takes the alert ink - but muted, because a run
   losing candidates is normal and expected, not an error. */
.funnel__drop{
  font-size:var(--size-caption);
  text-align:right;
  color:var(--colour-ink-faint);
  font-variant-numeric:tabular-nums;
}

@media (max-width:600px){
  .funnel__stage{grid-template-columns:74px 1fr 36px 36px;gap:var(--space-tight)}
}

/* AN OBJECT REFERENCE, wherever one appears in a table or a list. Follows the
   page's link convention exactly: inherits the ink, no underline at rest,
   underlines on hover. Without this the browser default rendered them blue and
   permanently underlined, which read as an error rather than as an
   affordance. */
.object-link{
  display:block;
  color:inherit;
  text-decoration:none;
}

.object-link:hover .object-link__accession{text-decoration:underline}
.object-link:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

.object-link__accession{
  display:block;
  font-family:var(--family-code);
  font-size:var(--size-caption);
}

.data-table{
  width:100%;
  border-collapse:collapse;
  font-size:var(--size-detail);
  margin:0 0 6px;
}

.data-table__column-heading{
  text-align:left;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:400;
  padding:0 10px 7px 0;
  border-bottom:1px solid var(--colour-rule);
}

.data-table__row{border:0}

.data-table__cell{
  padding:9px 10px 9px 0;
  border-bottom:1px solid var(--colour-rule);
  vertical-align:top;
}

.data-table__cell--absent{color:var(--colour-ink-faint);font-size:var(--size-detail)}

.data-table__row--failed .data-table__cell{color:var(--colour-alert)}

.data-table__annotation{
  display:block;
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
}

.data-table__annotation--code{font-family:var(--family-code)}

.data-table__seed-tag{
  font-style:normal;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  padding:0 var(--space-hair);
  margin:0 0 0 5px;
  border:1px solid var(--colour-rule);
}

/* ============ LITANY ============
   A litany is 5-7 objects that rhyme rather than contrast, each carrying one
   line in a repeating grammar. The repetition IS the argument, so the lines are
   set as a list of equals - no bullets, no numbering, nothing that would imply
   sequence or rank between them. They sit above the closing narrative, which
   only names what the set is about.

   Every value is an existing token. Ten var() references in this file once
   pointed at tokens that did not exist and resolved to nothing, silently: var()
   with no fallback fails without an error. */
.litany{
  display:grid;
  gap:var(--space-hair);
  max-width:var(--measure-prose);
  margin:0;
  padding-left:var(--space-base);
  border-left:2px solid var(--colour-rule);
}

.litany__line{
  margin:0;
  font-family:var(--family-display);
  font-size:var(--size-body);
  line-height:var(--leading-normal);
  color:var(--colour-ink);
}

.litany__line:last-child{margin-bottom:0}

/* ============ MODERN ECHO ============
   A PROPOSAL the writer offered, not part of the encounter: no photograph backs
   it and it is not a member. Deliberately styled UNLIKE an encounter — dashed
   rule, muted ink, small label first — because anything that reads as finished
   museum copy would imply a claim the museum has not made and cannot yet
   support. The label is the load-bearing part and comes before the content.

   Every value is an existing token; ten var() references in this file once
   resolved to nothing because the tokens did not exist. */
.echo{
  display:grid;
  gap:var(--space-hair);
  max-width:var(--measure-prose);
  margin:0;
  padding:var(--space-base);
  border:1px dashed var(--colour-rule);
  background:var(--colour-surface-sunken);
}

.echo__label{
  margin:0;
  font-family:var(--family-code);
  font-size:var(--size-caption);
  letter-spacing:.04em;
  text-transform:uppercase;
  color:var(--colour-ink-muted);
}

.echo__thing{
  margin:0;
  font-family:var(--family-display);
  font-size:var(--size-body);
  line-height:var(--leading-normal);
  color:var(--colour-ink);
}

.echo__job{
  margin:0;
  font-size:var(--size-detail);
  line-height:var(--leading-normal);
  color:var(--colour-ink-muted);
}

.arm-picker{
  display:flex;
  flex-direction:column;
  margin:0;
}

.arm{
  display:grid;
  grid-template-columns:auto auto 1fr;
  gap:var(--space-tight);
  align-items:center;
  padding:6px 0;
  border-top:1px solid var(--colour-rule-soft);
  cursor:pointer;
}

.arm:first-of-type{border-top:none}

.arm__checkbox{
  margin:0;
  accent-color:var(--colour-ink);
}

/* The letter names the row so it can be referred to aloud. Caption size and faint ink: it is a handle, not part of the sentence beside it. */
/* THE TAG: checkbox and letter in one tinted cell, carrying the SAME five colours as the list tags, so the key is taught here and used there. Reading the two as one key only works if they agree. */
.tag{
  display:inline-block;
  min-width:18px;
  padding:1px 5px;
  border-radius:2px;
  background:var(--colour-surface-sunken);
  font-size:var(--size-caption);
  line-height:1.5;
  text-align:center;
  color:var(--colour-ink);
}

.arm .tag{justify-self:start}

.tag--a{background:var(--colour-arm-a)}
.tag--b{background:var(--colour-arm-b)}
.tag--c{background:var(--colour-arm-c)}
.tag--d{background:var(--colour-arm-d)}
.tag--e{background:var(--colour-arm-e)}

.arm__verb{
  font-size:var(--size-detail);
  line-height:var(--leading-normal);
  color:var(--colour-ink);
}

.arm--off .arm__verb{color:var(--colour-ink-muted)}

.arm__checkbox:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

/* Skipped says "not selected", never a zero or an empty bar: a stage with no measurement must not read as a measured zero. */

/* ---------- running now ---------- Present ONLY while something is working, for anyone: a run belongs to the machine, not to the tab that started it. The section is removed entirely when idle rather than showing an empty state - a panel announcing "nothing is running" is furniture that never earns its space. */

.runs{
  padding:var(--space-comfy) 0;
  display:flex;
  flex-direction:column;
  gap:var(--space-snug);
  border-bottom:1px solid var(--colour-rule);
}

.runs__heading{
  margin:0;
  padding:0 var(--gutter);
}

/* The heading IS the control, so it spans the column and the caret sits on the
   right edge - a small target beside a wide inert label is the worse shape. */
.runs__toggle{
  display:flex;
  justify-content:space-between;
  align-items:center;
  width:100%;
  padding:0;
  border:0;
  background:none;
  font:inherit;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:700;
  text-align:left;
  cursor:pointer;
}

.runs__toggle:hover span:first-child{text-decoration:underline}

.runs__toggle:focus-visible{
  outline:2px solid var(--colour-ink);
  outline-offset:2px;
}

/* Rotated rather than swapped for a second glyph: one character that turns is
   read as the same control changing state. */
.runs__caret{
  display:inline-block;
  transition:transform .15s ease;
  transform:rotate(-90deg);
}

.runs__caret--open{transform:rotate(0deg)}

/* The list is a <template x-if> wrapper, so the cards no longer sit directly in
   `.runs` and stopped inheriting its column gap - they stacked flush. Spacing is
   a gap on the parent, never a margin on the card (CLAUDE.md: an adjacent-sibling
   selector cannot match across an x-if boundary anyway). */
.runs__list{
  display:flex;
  flex-direction:column;
  gap:var(--space-snug);
}

/* THE WHOLE CARD IS THE HOVER SURFACE. The button inside is the control, but
   the thing that visibly reacts is the card - so the tint and the cursor both
   sit here, or the pointer would show over the title while the colour changed
   around it. `:hover` on the card already covers the button, which is a
   descendant, so no second selector is needed.

   AT REST THE CARD BREATHES between the two tints, because a run works for
   minutes with nothing else on screen moving and a still card is
   indistinguishable from a stalled one. It is a liveness signal, not a
   progress bar: the real progress is the step count inside. */
.runs__item{
  display:grid;
  gap:var(--space-tight);
  margin:0 var(--gutter);
  border:1px solid var(--colour-rule);border-radius:3px;
  padding:var(--space-snug);background:var(--colour-highlight-open);
  cursor:pointer;
  transition:background-color .12s ease;
  animation:runs-breathe var(--runs-breathe-period) ease-in-out infinite;
}

/* Symmetric, so the fade in and the fade out take the same time and the card
   never appears to snap back. Both ends hold briefly - a pulse that is only
   ever mid-transition reads as a flicker rather than a breath. */
@keyframes runs-breathe{
  0%,   12%  {background-color:var(--colour-highlight-open)}
  50%,  62%  {background-color:var(--colour-highlight-new)}
  100%       {background-color:var(--colour-highlight-open)}
}

/* Hover WINS over the pulse and stops it. A surface answering the pointer must
   settle where the pointer put it; a card still cycling under the cursor reads
   as though the hover did not register. Same blue a newly-arrived encounter
   carries, so "this responds to you" reads the same way in both lists rather
   than as two unrelated conventions. */
.runs__item:hover{background:var(--colour-highlight-new);animation:none}

/* A looping animation is exactly what this setting exists to silence, and this
   one is decorative - nothing here is knowable ONLY from the pulse. */
@media (prefers-reduced-motion:reduce){
  .runs__item{animation:none}
}

/* THE TITLE IS THE CONTROL. Button resets, because this was a <p>; the card
   is a heading over a list of arms, so the heading is the thing you click. */
.runs__prompt{
  display:block;
  width:100%;
  margin:0;
  padding:0;
  border:0;
  background:none;
  font:inherit;
  font-size:var(--size-body);
  color:var(--colour-ink);
  text-align:left;
  cursor:inherit;
}

.runs__prompt:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

.runs__arms{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.runs__arm{display:flex;gap:var(--space-tight);align-items:baseline;font-size:var(--size-detail)}

.runs__arm-name{flex:0 0 auto}
.runs__arm--done .runs__arm-stage,
.runs__arm--done .runs__arm-step{color:var(--colour-ink-faint)}
.runs__arm--failed .runs__arm-stage{color:var(--colour-alert)}
.runs__arm-stage{color:var(--colour-ink);flex:1}
/* tabular figures so 3/5 and 10/10 occupy the same width and the column does not shuffle as a run advances */
.runs__arm-step{color:var(--colour-ink-faint);font-variant-numeric:tabular-nums}

.run-error{
  margin:0 var(--gutter) var(--space-comfy);padding:var(--space-snug);
  border:1px solid var(--colour-alert);border-radius:3px;
  color:var(--colour-alert);font-size:var(--size-detail)
}

/* ======================================================================
   PAGE: objects
   ================================================================== */

/* ========================================================================
   BLOCK: object-list — search results, the left column
   Structurally the sibling of .encounter-list, but the rows are not the same
   shape: an encounter row is all text, an object row leads with a picture. The
   two are kept as separate blocks rather than one with modifiers, because
   "a list of things you can open" is not a strong enough shared idea to be
   worth coupling them.
   ===================================================================== */
.object-list{
  display:grid;
  grid-template-columns:minmax(0, 1fr);
}

.object-list__empty{
  font-size:var(--size-body);
  color:var(--colour-ink-faint);
  padding:24px var(--gutter);
  margin:0;
}

/* Picture then text. Grid rather than flex so the text column is bounded by minmax(0,1fr) - without that, a long unbroken title would push the row wider than the column and force a horizontal scrollbar on a scroll container that must never have one. */
.object-list__entry{
  display:grid;
  grid-template-columns:100px minmax(0, 1fr);
  gap:var(--space-snug);
  align-items:start;
  width:100%;
  text-align:start;
  font:inherit;
  padding:var(--space-snug) var(--gutter);
  border:0;
  border-top:1px solid var(--colour-rule);
  background:none;
  cursor:pointer;
}

.object-list__entry:focus-visible{outline:2px solid var(--colour-ink);outline-offset:-2px}
.object-list__entry:hover .object-list__title{text-decoration:underline}

.object-list__entry--open{background:var(--colour-highlight-open);border-top-color:transparent}
.object-list__entry--open + .object-list__entry{border-top-color:transparent}

.object-list__thumb{
  width:100px;
  height:100px;
  object-fit:cover;
  background:var(--colour-surface-sunken);
  border:1px solid var(--colour-rule);
}

.object-list__text{
  display:grid;
  gap:2px;
  min-width:0;          
}

.object-list__title{
  font-family:var(--family-display);
  font-size:var(--size-title);
  line-height:1.25;
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

.object-list__essence{
  font-size:var(--size-detail);
  color:var(--colour-ink-muted);
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

.object-list__facets,
.object-list__facets,
.object-list__facets,
.object-list__accession{
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.object-list__accession{font-family:var(--family-code)}

/* ========================================================================
   BLOCK: object — one object, read in full (the right column)
   ===================================================================== */
.object__placeholder{
  font-family:var(--family-display);
  font-size:var(--size-title);
  color:var(--colour-ink-faint);
  max-width:var(--measure-note);
  margin:0;
}

.object__placeholder--error{color:var(--colour-alert)}

/* The object record stacks a header, the photographs and its panels. Same rule
   as `.encounter`: the container owns the spacing, the blocks inside do not. */
.object{display:grid;gap:var(--space-comfy)}

.object__header{margin:0}

.object__title{
  font-family:var(--family-display);
  font-size:var(--size-display);
  line-height:var(--leading-tight);
  font-weight:500;
  margin:0 0 var(--space-hair);
  max-width:var(--measure-prose);
  display:-webkit-box;
  -webkit-line-clamp:3;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

.object__accession{
  font-family:var(--family-code);
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
  margin:0 0 var(--space-snug);
}

/* The provenance of a record — which prompt version and model wrote it. Sits inside the heading so the claim and its author are never separated. */
.panel__provenance{
  font-family:var(--family-code);
  font-size:var(--size-caption);
  text-transform:none;
  letter-spacing:0;
  font-weight:400;
  color:var(--colour-ink-faint);
  margin-left:var(--space-tight);
}

.panel__description{
  font-size:var(--size-detail);
  line-height:var(--leading-loose);
  color:var(--colour-ink-muted);
  max-width:var(--measure-note);
  margin:var(--space-snug) 0 0;
}

.panel__none{
  font-size:var(--size-detail);
  color:var(--colour-ink-faint);
  max-width:var(--measure-note);
  margin:0;
}

/* ========================================================================
   BLOCK: used-in — the encounters that used this object
   A LINK, not a button: it leaves for the other page, so it must behave like a
   link (open in a new tab, copy address, show its target in the status bar).
   ===================================================================== */
.used-in{
  display:grid;
  gap:2px;
  padding:var(--space-snug) 0;
  border-top:1px solid var(--colour-rule);
  color:inherit;
  text-decoration:none;
}

.used-in:hover .used-in__title{text-decoration:underline}
.used-in:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

.used-in__title{
  font-family:var(--family-display);
  font-size:var(--size-title);
  line-height:1.25;
}

.used-in__narrative{
  font-size:var(--size-detail);
  color:var(--colour-ink-muted);
  display:-webkit-box;
  -webkit-line-clamp:2;
  -webkit-box-orient:vertical;
  overflow:hidden;
}

.used-in__meta{
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
}

@media (max-width:900px){
  
  .object-list__entry{grid-template-columns:72px minmax(0, 1fr)}
  .object-list__thumb{width:72px;height:72px}
}

/* ========================================================================
   BLOCK: filters — narrowing the collection
   Four filters, and only four: department has 5 values and medium 8, which fit
   a dropdown, while culture has 392 distinct values and classification 186 -
   unusable as a list, and already reachable through the search box.
   ===================================================================== */
.filters{
  display:grid;
  grid-template-columns:repeat(2, minmax(0, 1fr));
  gap:var(--space-tight) var(--space-snug);
  margin:0;
}

.filters__field{display:grid;gap:2px;min-width:0}

.filters__field--years{grid-column:1 / -1}

.filters__label{
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
}

.filters__select,
.filters__year{
  width:100%;
  font:inherit;
  font-size:var(--size-detail);
  color:var(--colour-ink);
  padding:6px 8px;
  border:1px solid var(--colour-rule);
  border-radius:0;
  background:var(--colour-page);
}

.filters__select:focus-visible,
.filters__year:focus-visible{outline:2px solid var(--colour-ink);outline-offset:-1px}

.filters__range{
  display:grid;
  grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
  align-items:center;
  gap:var(--space-tight);
}

.filters__dash{color:var(--colour-ink-faint)}

.filters__state{
  display:flex;
  align-items:baseline;
  gap:var(--space-tight);
  flex-wrap:wrap;
  font-size:var(--size-caption);
  color:var(--colour-ink-muted);
  margin:0;
}

/* Underline, not border-bottom. A border sits at the bottom of the box rather than on the text baseline, so it reads as a different mark from every other underlined control in the app. */
.filters__clear{
  font:inherit;
  font-size:var(--size-caption);
  color:var(--colour-ink);
  padding:0;
  border:0;
  text-decoration:underline;
  background:none;
  cursor:pointer;
}

.filters__clear:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

/* ========================================================================
   BLOCK: panel — the objects page's own sections
   The `record`/`disclosure` families this block once extended are gone: the
   provenance panel stopped nesting object records inside itself and links out
   to the objects page instead, which already shows all of it with the
   photograph. One place per object.
   ===================================================================== */

.panel__heading{
  display:flex;
  align-items:baseline;
  flex-wrap:wrap;
  gap:var(--space-tight);
  font-family:var(--family-text);
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-faint);
  font-weight:700;
  margin:0 0 var(--space-snug);
}

.plate__view{
  font-size:var(--size-caption);
  color:var(--colour-ink-faint);
}

@media (max-width:700px){
  .filters{grid-template-columns:minmax(0,1fr)}
  .object__title{font-size:24px}
}

.object__back{
  display:inline-block;
  font-size:var(--size-caption);
  letter-spacing:var(--tracking-caption);
  text-transform:uppercase;
  color:var(--colour-ink-muted);
  text-decoration:underline;
  margin:0 0 var(--space-base);
}

.object__back:hover{color:var(--colour-ink)}
.object__back:focus-visible{outline:2px solid var(--colour-ink);outline-offset:2px}

.object-list__entry:first-of-type{border-top:0}

.encounter-list__entry:first-of-type{border-top:0}
