/* foldjs shared UI
   =================
   One stylesheet for every app in this repository. CIRPIN's values are canonical --
   SoftAlign was copied from it and drifted, and this is where that stops: 30 class
   names were shared between the two pages and 22 of them had different rules, which
   is copy-then-drift in its purest form. The two apps ask the same shape of question
   about a structure and a hit, so a reader moving between them should not have to
   relearn where anything is.

   WHAT BELONGS HERE. The design tokens, the page chrome, the two-column layout, the
   three interaction signals, the panel tabs, the results table, the domain strip and
   the hit card -- anything whose look is a property of "an app in foldjs" rather than
   of one app's subject matter.

   WHAT DOES NOT. A page's own furniture: CIRPIN's score plane, colour ramps and
   easter egg; SoftAlign's search block and alignment swatches. Those stay in the
   page. A rule only one app will ever use does not belong here just to look tidy.

   The three interaction signals are load-bearing and documented at "what is
   interactive". Anything added later picks one of the three rather than a fourth. */


/* --- tokens, reset and the page ---------------------------------------------- */
  :root {
    --paper:  #edf0f4;
    --panel:  #f8fafc;
    --ink:    #16202e;
    --ink-2:  #5a6a7d;
    --rule:   #c3ccd8;
    --rule-2: #dde3ea;
    --cut:    #d6006e;
    --bond:   #0e7c86;
    --flag:   #b8791a;
    --sel:    #e8a400;   /* current selection, distinct from --cut */

    --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
    --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  }

  * { box-sizing: border-box; }

  /* Any rule setting `display` beats the user agent's [hidden] rule, so every
     flex or grid container in here silently ignored the attribute — the tab strip,
     the toggle rows and the chain picker each shipped visible while marked hidden.
     One declaration instead of remembering to guard every new container. */
  [hidden] { display: none !important; }

  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font: 15px/1.55 var(--sans);
    -webkit-font-smoothing: antialiased;
  }

/* --- header, toolbar and status strip ---------------------------------------- */
  /* One toolbar: identity, the two ways in, and which database. Everything that
     starts a search lives here, so the columns below hold only the answer. */
  header {
    position: relative;
    padding: 10px 18px 9px; border-bottom: 1px solid var(--rule);
  }
  .bar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
  .tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  .tools #pdbId { width: 104px; flex: 0 0 auto; padding: 6px 9px; font-size: 12px; }
  .tools button { padding: 6px 11px; font-size: 11px; }
  /* Not scoped to the toolbar: an optional action is an optional action wherever it sits. */
  .ghost { background: none; color: var(--ink-2); border-color: var(--rule); }
  .ghost:hover:not(:disabled) { background: var(--cut); border-color: var(--cut); color: #fff; }
  .pick > span { font-size: 10px; }
  /* The Search button reads as the primary action only when scoring is owed. */
  #searchBtn { padding: 6px 13px; font-size: 11px; }
  #searchBtn:disabled { opacity: .35; }
  #searchBtn.due { background: var(--cut); border-color: var(--cut); }
  /* Secondary controls: they change how the plot above is drawn, not which plot it
     is, so they are text rather than another row of filled segments. */
  /* An inline choice, not a sentence.
     These read as prose when set as plain words with a dotted underline: "colour by alignment
     order residue number" looked like a caption that happened to contain emphasis. Giving each
     option the same chip shape the rest of the app uses for controls — a bordered box on the
     panel surface, filled when active — makes it unmistakably a thing to press, and makes the
     ACTIVE one legible at a glance rather than by noticing which word is bold. */
  .opt-row {
    display: flex; align-items: center; gap: 8px; margin: 9px 0 0;
    font: 11px var(--mono); letter-spacing: .06em; color: var(--ink-2);
  }
  /* Label, in the same voice as every other section label, so the line reads
     "COLOUR BY [chip] [chip]" rather than as a sentence with two emphasised phrases. */
  .opt-row > span {
    font: 500 10px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase;
  }
  .opt {
    background: var(--panel); border: 1px solid var(--rule); padding: 4px 9px;
    color: var(--ink-2); font: 11px var(--mono); letter-spacing: .06em; text-transform: none;
    cursor: pointer;
  }
  .opt:hover:not([aria-pressed="true"]) { background: #fff; color: var(--ink); border-color: var(--ink-2); }
  .opt[aria-pressed="true"] {
    background: var(--cut); border-color: var(--cut); color: #fff; font-weight: 500;
  }

  /* Its own strip below the toolbar, always present. Progress and the idle state
     share one line, so nothing appears or disappears and the page never shifts
     while a search runs. */
  #statusbar {
    position: relative; padding: 6px 18px 7px;
    border-bottom: 1px solid var(--rule); background: var(--panel);
    min-height: 27px; box-sizing: border-box;
  }
  /* Sits on the status strip's bottom border, out of flow, so appearing and
     disappearing cannot shift the page. */
  #progTrack {
    position: absolute; left: 0; right: 0; bottom: -1px; height: 3px;
    background: transparent; overflow: hidden; pointer-events: none;
  }
  #progTrack.on { background: var(--rule-2); }
  /* Width is driven per frame from requestAnimationFrame, so a CSS transition
     here would fight the animation rather than smooth it. */
  #progTrack i { display: block; height: 100%; width: 0; background: var(--cut); }
  #progTrack.indeterminate i { width: 28%; animation: slide 1.1s ease-in-out infinite; }
  @keyframes slide { 0% { margin-left: -28%; } 100% { margin-left: 100%; } }
  @media (prefers-reduced-motion: reduce) {
    #progTrack.indeterminate i { animation: none; width: 100%; opacity: .5; }
    #progTrack i { transition: none; }
  }
  #status b { color: var(--ink); font-weight: 600; }
  #status .detail { color: var(--ink-2); }
  .wordmark { font: 600 15px/1 var(--mono); letter-spacing: .34em; text-transform: uppercase; }
  .wordmark span { color: var(--cut); }
  /* Pushed to the right edge of the bar, and quiet: a running total is for glancing at. */
  .net {
    margin-left: auto; font: 11px var(--mono); letter-spacing: .06em; color: var(--ink-2);
    white-space: nowrap; cursor: default;
  }
  #status { font: 12px/1 var(--mono); color: var(--ink-2); display: flex; align-items: center; gap: 8px; }
  #status::before {
    content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--flag); flex: none;
  }
  #status.ready::before { background: var(--bond); }

/* --- layout: two columns, and the one-column fallback ------------------------ */
  main { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
  /* Sticks while the results scroll past it. Picking a hit is an instruction to
     look at something, so the thing being looked at must not scroll away to
     reach the row that selects it. */
  /* Sticky, but not its own scroller. It used to cap at 100vh and scroll inside itself, which
     put a second scrollbar on the page for content that now fits: the readout below the structure
     was six labelled rows and is one wrapping line. If it ever does overflow the page scrolls,
     which is the behaviour people expect from a column of prose and a picture. */
  /* Sticky, and align-self: start, so its height is its own content rather than the row's. That is
     what a column which must not scroll away needs — and it is why the divider cannot live on this
     column: a border-right on a short sticky box is a short line, stopping wherever the structure
     happens to end. */
  .view-col {
    padding: 14px 18px 24px; min-width: 0;
    position: sticky; top: 0; align-self: start;
  }
  /* The divider, on the column that DOES span the row. A grid item stretches by default, so this one
     is as tall as the taller of the two columns, and its left border runs the whole way down. */
  .work-col { padding: 14px 18px 30px; min-width: 0; border-left: 1px solid var(--rule); }
  /* Nothing to divide from when the left column is not there. */
  body.no-structure .work-col { border-left: 0; }
  body.no-structure .view-col { display: none; }
  body.no-structure main { grid-template-columns: minmax(0, 1fr); }
  /* Centred, because on first load this column is the whole page. */
  body.no-structure .work-col { max-width: 680px; margin: 0 auto; }
  #hero { display: none; text-align: center; padding: 6px 0 4px; }
  body.no-structure #hero { display: block; }
  /* Square, and as large as the column allows. It matches the game board it turns into — same size,
     same ring, same segment pitch — so clicking it does not resize the animal. */
  #heroCanvas {
    width: 100%; max-width: 440px; aspect-ratio: 1; height: auto;
    display: block; margin: 0 auto;
  }
  .hero-cue {
    margin: 14px auto 0; max-width: 500px;
    font: 11px/1.6 var(--mono); letter-spacing: .06em; color: var(--ink-2);
  }
  .hero-cue b { color: var(--cut); font-weight: 600; }
  /* 760px, not 1000.
     At 1000 a laptop window dragged to two thirds of the screen already collapsed to a single
     column, which throws away the side-by-side comparison the layout exists for. The two panels
     stay usable much narrower than that: the work column needs about 360px for the results table,
     and the table scrolls sideways below that rather than forcing the whole page to reflow.
     Two columns are given up only when there is genuinely no room for two. */
  @media (max-width: 760px) {
    /* One column, structure first, results under it.
       It used to be the other way round, on the reasoning that a viewer is useless until something
       has been loaded so the way in should not be below an empty canvas. That stopped being true
       when the PDB field and Upload moved into the header: the way in is now always at the top of
       the page, and what belongs directly under it is the thing you just loaded. Reading order is
       then load, look, then the hits — the same order the two columns have on a wide screen, top to
       bottom instead of left to right. */
    main { grid-template-columns: 1fr; }
    .view-col { order: 1; position: static; max-height: none; overflow: visible;
      border-bottom: 1px solid var(--rule); }
    /* Stacked, so the divider is a rule above the results rather than beside them. */
    .work-col { order: 2; border-left: 0; }
  }

/* --- section labels and captions --------------------------------------------- */
  .eyebrow {
    font: 500 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase;
    color: var(--ink-2); margin: 0 0 8px;
  }
  .eyebrow.spaced, .toggle-row.spaced { margin-top: 13px; }
  .hint { color: var(--ink-2); font-size: 12px; line-height: 1.45; margin: 7px 0 0; }
  .hint b { color: var(--ink); font-weight: 600; }
  /* A hint that has been emptied still held its top margin, so trimming the text left the gap
     behind. :empty collapses the element instead of leaving a ghost of it. */
  .hint:empty, .legend:empty { display: none; margin: 0; }

/* --- what scrolls ------------------------------------------------------------ */
  /* --- what scrolls -------------------------------------------------------
     One region scrolls independently of the page: the results table. On macOS its scrollbar was
     an overlay, appearing only while scrolling and vanishing after — so a pane that scrolls looked
     exactly like one that does not, and the only way to find out was to try. Styling the bar is
     what makes it permanent: a webkit scrollbar with any rule applied stops being an overlay.
     Styled native bars, not scripted ones. A div pretending to be a scrollbar loses keyboard
     paging, touch momentum, and the screen-reader behaviour that comes free here; the appearance
     is the only part worth taking over. The thumb's length also does useful work, since it says
     how much more there is — which no static hint can. */
  .table-wrap { scrollbar-width: thin; scrollbar-color: var(--rule) transparent; }
  .table-wrap::-webkit-scrollbar { width: 11px; height: 11px; }
  .table-wrap::-webkit-scrollbar-track { background: transparent; }
  .table-wrap::-webkit-scrollbar-thumb {
    background: var(--rule); border-radius: 6px;
    /* inset by a transparent border so the thumb reads as floating in the gutter rather than
       filling it, which is what keeps an always-visible bar quiet */
    border: 3px solid transparent; background-clip: padding-box;
  }
  .table-wrap::-webkit-scrollbar-thumb:hover { background: var(--ink-2); background-clip: padding-box; }
  .table-wrap::-webkit-scrollbar-corner { background: transparent; }

/* --- what is interactive ----------------------------------------------------- */
  /* --- what is interactive -------------------------------------------------
     Three signals, and nothing else in the page uses them:

       a filled box the action the page is waiting for. Plain buttons: Fetch, Search.
       an outlined box something you may do, not something owed. .ghost — Upload, Auto parse.
                    A filled button reads as an instruction, so anything optional has to be
                    outlined or it collects presses it did not need.
                    Also .seg (segmented groups) and .opt (inline choices), filled when active.
       underlined   a link out. Real anchors, and the domain name in the readout.
       unboxed grey a caption. .hint, .legend, .note, .eyebrow — never clickable.

     This existed implicitly and one family broke it: .opt was set as bare words with a dotted
     underline, so "colour by alignment order residue number" read as a sentence containing
     emphasis rather than as a label and two buttons. Anything added later should pick one of the
     three rather than invent a fourth. */

  .seg { display: inline-flex; border: 1px solid var(--rule); }
  .seg button {
    background: var(--panel); border: 0; color: var(--ink-2); padding: 6px 12px;
    font: 11px var(--mono); letter-spacing: .1em; text-transform: uppercase;
  }
  .seg button + button { border-left: 1px solid var(--rule); }
  .seg button[hidden] + button { border-left: 0; }
  .seg button[aria-pressed="true"] { background: var(--cut); color: #fff; }
  .seg button:hover:not([aria-pressed="true"]) { background: #fff; color: var(--ink); }
  .toggle-row { display: flex; align-items: center; gap: 10px; margin: 0 0 9px; flex-wrap: wrap; }
  .toggle-row .note { font: 11.5px var(--mono); color: var(--ink-2); }

  .tabs { display: flex; margin: 0 0 6px; border-bottom: 1px solid var(--rule); }
  .tab {
    background: none; border: 0; border-bottom: 2px solid transparent; border-radius: 0;
    padding: 0 0 7px; margin-right: 18px; color: var(--ink-2); cursor: pointer;
    font: 500 11px/1 var(--mono); letter-spacing: .16em; text-transform: uppercase;
  }
  .tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--cut); }
  .tab:hover:not(:disabled) { color: var(--ink); background: none; }
  .tab:disabled { opacity: .35; cursor: default; }

/* --- empty states and readouts ----------------------------------------------- */
  .empty {
    position: absolute; inset: 0; display: grid; place-content: center; text-align: center;
    color: var(--ink-2); font-size: 13px; max-width: 280px; margin: 0 auto;
  }
  .readout:empty { display: none; }
  .readout { margin: 6px 0 0; min-height: 15px; font: 11px/1.4 var(--mono); color: var(--ink-2); }
  .legend {
    margin: 8px 0 0; display: flex; gap: 13px; flex-wrap: wrap;
    font: 11px/1.6 var(--mono); color: var(--ink-2);
  }
  .legend i { display: inline-block; width: 14px; height: 2px; vertical-align: middle; margin-right: 6px; }
  .legend i.dot { width: 7px; height: 7px; border-radius: 50%; }
  /* The key for a heatmap: paper at zero, an accent at one. The far end is a variable because a
     ramp has to match the map it explains, and the two apps paint different quantities in
     different accents -- CIRPIN's maps run to teal, SoftAlign's alignment runs to pink. A legend
     whose colours disagree with the picture beside it is worse than no legend, so this is set
     where the ramp is used rather than guessed here. */
  .ramp {
    display: inline-block; width: 54px; height: 7px; vertical-align: middle; margin: 0 6px;
    background: linear-gradient(90deg, var(--paper), var(--ramp-end, var(--bond)));
    border: 1px solid var(--rule);
  }

/* --- disclosure -------------------------------------------------------------- */
  .fold { margin: 18px 0 0; border-top: 1px solid var(--rule); padding-top: 8px; }
  .fold > * { margin-top: 8px; }
  .fold > .eyebrow { margin-top: 0; }

/* --- inputs, selects and buttons --------------------------------------------- */
  #dropVeil {
    position: fixed; inset: 0; z-index: 200; display: none; place-content: center;
    background: rgba(237, 240, 244, .93); border: 2px dashed var(--cut);
  }
  #dropVeil.on { display: grid; }
  #dropVeil span {
    font: 500 15px var(--mono); letter-spacing: .06em; color: var(--ink);
  }
  .drop b { display: block; color: var(--ink); font-weight: 600; margin-bottom: 3px; }
  .fetch { flex: 1 1 200px; display: flex; flex-direction: column; gap: 8px; justify-content: center; }
  input[type=text] {
    flex: 1; min-width: 0; padding: 9px 11px; border: 1px solid var(--rule); background: var(--panel);
    font: 13px var(--mono); color: var(--ink); border-radius: 0;
  }
  input[type=text]:focus-visible, button:focus-visible, .drop:focus-visible,
  select:focus-visible {
    outline: 2px solid var(--cut); outline-offset: 2px;
  }
  /* The database picker. A select rather than a pair of buttons because the
     options are not equivalent — one is already in memory and the other is a
     277 MB download — and a menu can say so in the label where a toggle cannot. */
  .pick { display: inline-flex; align-items: center; gap: 8px; }
  .pick > span {
    font: 500 11px var(--mono); letter-spacing: .12em; text-transform: uppercase;
    color: var(--ink-2);
  }
  select {
    padding: 7px 28px 7px 10px; border: 1px solid var(--rule); background: var(--panel);
    font: 12px var(--mono); color: var(--ink); border-radius: 0; cursor: pointer;
    appearance: none;
    background-image: linear-gradient(45deg, transparent 50%, var(--ink-2) 50%),
                      linear-gradient(135deg, var(--ink-2) 50%, transparent 50%);
    background-position: calc(100% - 15px) 51%, calc(100% - 10px) 51%;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
  }
  select:hover:not(:disabled) { background-color: #fff; }
  select:disabled { opacity: .55; cursor: progress; }
  button {
    padding: 9px 15px; border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
    font: 500 12px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase;
    cursor: pointer; border-radius: 0;
  }
  button:hover:not(:disabled) { background: var(--cut); border-color: var(--cut); }
  button:disabled { opacity: .4; cursor: default; }

/* --- the domain strip -------------------------------------------------------- */
  .domains { margin: 14px 0 0; }
  .domains-head { display: flex; align-items: center; gap: 12px; }
  .domains-head .eyebrow { margin: 0 auto 0 0; }
  /* With the heading hidden there is nothing pushing the button right, so the row
     itself does the pushing and the button keeps its position either way. */
  .domains-head { justify-content: flex-end; }
  /* A panel's heading beside whatever controls belong to it, baselines aligned so the label
     and the buttons sit on one line rather than being centred against each other. */
  .results-head { display: flex; align-items: baseline; gap: 12px; }
  /* The one action in a panel, so it wears the app's real button rather than looking like a
     link. Toolbar-sized: it is the primary thing in its panel, but not on the page. */
  .act { padding: 6px 11px; font-size: 11px; }
  /* A compact picker inside a results heading -- "SHOW top 10", "RERANK top 100". Sits beside the
     count it governs rather than in the controls above, because it changes the list and not the
     query.
     NO CHEVRON on this one. The base select draws its arrow with two gradients at
     calc(100% - 15px) and reserves 28px of right padding for it; this override tightens the
     padding to 6px, so the arrow was being painted straight over the text -- "top 10" came out
     with a wedge through the 0. It is a short picker next to its own label, so the affordance
     was costing more than it carried. */
  .hits-pick select { padding: 4px 6px; font-size: 11px; background-image: none; }
  .hits-pick > span { font-size: 10px; }
  #domainBars { margin-top: 10px; }
  /* Padding for the selection ring to sit in, and a negative margin of the same size so the
     CONTENT box is unchanged. That matters: #domainAxis below uses .axis-pad with an identical
     grid, and the ticks only line up with the bars while both have the same content width. Adding
     padding alone would have shifted every bar 8px and silently broken that alignment. */
  .dbar {
    display: grid; grid-template-columns: 22px 1fr 16px 22px;
    align-items: center; gap: 8px; padding: 5px 8px; margin: 0 -8px;
  }
  /* The selected domain is the one the viewer and the results are about, so it is
     boxed rather than only slightly brighter. An inset shadow, not a border,
     because a border would shift the row off the axis drawn underneath.
     Yellow: it means "this is what you have picked", and it is the one signal here
     that must not be confused with pink, which everywhere else means a cut or a
     committed action. */
  .dbar[data-active="true"] {
    background: #fff; box-shadow: inset 0 0 0 2px var(--sel);
  }
  /* The domain's colour, where its number used to be.
     A separate legend was tried and dropped: the rows already are the list of domains, one per line,
     so a key beside them was a second copy of the same list. Putting the colour on the row makes the
     row the key — and the number was never information, since the rows are in order and the bar
     underneath carries the residue range. */
  .dbar > .n {
    display: flex; align-items: center; justify-content: flex-end;
    cursor: pointer; user-select: none;
  }
  .dbar > .n i {
    display: block; width: 11px; height: 11px; border-radius: 50%;
    box-shadow: 0 0 0 1px rgba(22,32,46,.15) inset;
  }
  /* Unselected domains sit back, so the selected one is the one with a solid dot. */
  .dbar:not([data-active="true"]) > .n i { opacity: .45; }
  .dbar > .n:hover i { opacity: 1; }
  .dbar > svg { width: 100%; height: 22px; display: block; touch-action: none; }
  /* Empty unless the domain is too short to embed — a residue count here read
     as a coordinate, competing with the bar's own start and end. */
  .dbar > .warn { font: 600 12px/1 var(--mono); color: var(--cut); text-align: center; }
  .dbar .kill {
    background: none; border: 1px solid var(--rule); color: var(--ink-2);
    width: 20px; height: 20px; padding: 0; font: 13px/1 var(--mono); text-transform: none;
    letter-spacing: 0; display: grid; place-content: center;
  }
  .dbar .kill:hover:not(:disabled) { background: var(--cut); border-color: var(--cut); color: #fff; }
  .dbar .kill:disabled { opacity: .3; }
  .dbar { cursor: pointer; }
  #domainAxis { width: 100%; height: 16px; display: block; }
  .axis-pad { display: grid; grid-template-columns: 22px 1fr 16px 22px; gap: 8px; }
  #coRow { margin-top: 10px; }
  .co-cap { margin-top: 6px; }
  .co-wrap { position: relative; }
  #coassoc { display: block; margin: 0 auto; cursor: crosshair; }
  #domainRead { min-height: 14px; font-size: 11px; margin-top: 1px; }

/* --- the results table ------------------------------------------------------- */
  /* The list scrolls inside itself so choosing a row does not mean scrolling the
     structure views off the screen — you pick a hit in order to look at it. */
  .table-wrap {
    max-height: min(46vh, 460px); overflow-y: auto; overscroll-behavior: contain;
    /* Sideways rather than squeezing eight numeric columns into an unreadable width — and it is
       what lets the two-column layout survive down to 760px. */
    overflow-x: auto;
  }
  table { width: 100%; border-collapse: separate; border-spacing: 0;
          font-variant-numeric: tabular-nums; font-size: 12.5px; }
  th {
    text-align: right; font: 500 10px/1.3 var(--mono); letter-spacing: .12em; text-transform: uppercase;
    color: var(--ink-2); padding: 4px 8px 5px 0; border-bottom: 1px solid var(--rule);
    white-space: nowrap; cursor: pointer; user-select: none;
  }
  /* Stays put while the list scrolls under it. Needs an opaque background, since
     border-collapse: separate lets rows show through otherwise. */
  thead th { position: sticky; top: 0; z-index: 1; background: var(--paper); }
  th.left { text-align: left; }
  /* Residue count is three digits and never needs more. */
  th.narrow, td.narrow { width: 3.2em; padding-right: 4px; }
  /* Measured TM-score. The column is absent until the filter has produced one, so an
     unfiltered table is not carrying an empty column asking to be explained. */
  td.tm { white-space: nowrap; }
  /* Only the columns that can be sorted look clickable. TM is not one of them: it is
     measured for a subset, so ordering by it would rank the unmeasured against nothing. */
  th:not([data-sort]) { cursor: default; }
  th[data-sort]:hover { color: var(--ink); }
  th[aria-sort] { color: var(--ink); }
  /* The sorted column is marked by its colour and by aria-sort, not by a glyph. A per-column
     caret meant six columns each reserving 9px for a mark only one of them ever shows, and the
     direction it pointed was noise: each column has exactly one order that means anything, so the
     arrow never varied except between columns, where it read as a difference that was not there. */
  th[aria-sort] { font-weight: 600; }
  td {
    padding: 4.5px 8px 4.5px 0; border-bottom: 1px solid var(--rule-2);
    font: 12px var(--mono); text-align: right;
  }
  /* Both th and td, or the header drifts out of line with the body. */
  th:first-child, td:first-child { padding-left: 11px; }
  th:last-child, td:last-child { padding-right: 11px; }
  td.left { text-align: left; }
  tbody tr { cursor: pointer; }
  tbody tr:hover { background: var(--panel); }
  /* Selection is a box, not a tint.
     A 2px bar on the first cell was easy to miss on a row that is already coloured by its
     scores, and with the top hit now selected automatically it has to be obvious what the
     alignment below belongs to. --sel is the colour selection already means in the domain
     editor, so the vocabulary carries over.
     Outline on the ROW, which paints as one continuous rectangle. Outlining the cells instead
     drew a box around each one and turned a selected row into a grid; a border on the row is not
     an option because tr borders do not paint.
     Pulled 3px inside the row and paired with padding on the outer cells, so the rank number and
     the last column have air rather than sitting against the line. The same gap the domain rows
     use, so the two selections read as one idea. No corner radius on either: nothing else in the
     page is rounded, and rounding only the selection would make it look like a different kind of
     object. */
  /* The box, and only the box. A filled marker in the rank column and a bolded name were tried and
     dropped: two more things changing at once, to say one thing that a box already says.
     3px rather than 2: the score columns paint their own shading over the row tint, so on a selected
     row the tint survives only in the columns that have none, which leaves the outline carrying the
     state more or less alone — and at 2px it was competing with saturated teal and pink either side
     of it. */
  tbody tr[aria-selected="true"] {
    background: #fdf6e3; outline: 3px solid var(--sel); outline-offset: -3px;
  }
  tbody tr.inert { cursor: default; }
  tbody tr.inert:hover { background: none; }
  /* Reachable by a screen reader, invisible on screen. */
  .sr {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }

/* --- the hit card ------------------------------------------------------------ */
  /* The hit card. The app's own box idiom -- panel fill, one hairline rule -- so it reads as part of
     the page rather than a widget dropped onto it. It replaced a wrapping strip of chips, which fitted
     in one line but left the reader to work out which numbers belonged together. */
  .hitcard {
    margin: 11px 0 0; background: var(--panel); border: 1px solid var(--rule);
    font: 12px/1.5 var(--mono); color: var(--ink-2);
  }
  /* The id is the heading, so it takes the ink and the weight; the classification code is secondary and
     sits hard right, which is where someone looking for something to paste will look. */
  .hc-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
    padding: 7px 10px; border-bottom: 1px solid var(--rule-2);
  }
  .hc-head b { color: var(--ink); font-weight: 600; font-size: 13px; }
  .hc-head a { color: var(--bond); text-underline-offset: 2px; }
  .hc-code { font-size: 11px; color: var(--ink-2); white-space: nowrap; }
  /* The labelled block: what this hit IS, before any number about it. Two names for one grid,
     because the apps put different things in it and neither name would be honest for both --
     CIRPIN lists a classification (class, fold, superfamily, family), SoftAlign lists entry
     metadata (protein, organism, lengths, aligned range). The layout is the shared part.

     Label column sized to the LONGEST LABEL, not to a guess. max-content is what lets
     "Homologous superfamily" fit at any panel width while every row still lines up; a fixed
     width either clips that one or wastes the space on SCOPe's shorter words. */
  .hc-class, .hc-meta {
    display: grid; grid-template-columns: max-content 1fr; gap: 1px 12px;
    padding: 7px 10px; border-bottom: 1px solid var(--rule-2);
  }
  /* Nothing to say, no box: TED has no classification and a PDB hit has no organism, and an
     empty bordered strip reads as a failed lookup rather than as an absence. */
  .hc-class:empty, .hc-meta:empty { display: none; }
  .hc-class dt, .hc-meta dt { color: var(--ink-2); }
  /* Values are prose from the release files -- "Nucleotide-binding alpha-beta plait" -- so they set in
     the sans face, wrap rather than clip, and carry the ink, because they are the content. */
  .hc-class dd, .hc-meta dd {
    margin: 0; color: var(--ink); font-family: var(--sans); overflow-wrap: anywhere;
  }
  /* Side by side. Three short rows each -- two numbers and a delta -- fits where five did not, which is
     what made the first attempt at two columns cramped: dropping cut and aligned is what buys the room.
     minmax(120px, 1fr) still stacks the pair on a narrow panel rather than crushing it. */
  .hc-scores {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 2px 16px; padding: 7px 10px;
  }
  .hc-scores h4 {
    margin: 0 0 3px; font: 500 10px/1.4 var(--sans); letter-spacing: .12em;
    text-transform: uppercase; color: var(--ink-2);
  }
  .align-row { display: flex; gap: 6px; font: 12px/1.55 var(--mono); }
  .align-row span { color: var(--ink-2); }
  /* Numbers hard right, so a column of them can be read down rather than hunted for. */
  .align-row b {
    margin-left: auto; color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums;
  }
  .align-row a { color: var(--bond); text-underline-offset: 2px; }
  /* Side by side while there is room, stacked when there is not. The maps carry an aspect ratio that
     means something -- a permuted alignment is two segments at one slope -- so they are never squeezed
     to fit; the pair wraps instead.
     FLEX, NOT GRID with auto-fit. `repeat(auto-fit, minmax(190px, 1fr))` sizes the TRACKS from the
     container and not from the children, so in an 811px panel it lays out four 190px columns whatever
     is in them: two maps happened to look right, and one map got a quarter of the width and rendered
     at 195px. `flex: 1 1 190px` asks each cell for 190px and lets it grow, so one fills the row, two
     split it, and either wraps when 190px each is no longer available -- which is what the comment
     above always claimed the rule did. */
  .map-pair { display: flex; flex-wrap: wrap; gap: 8px 10px; }
  .map-cell { flex: 1 1 190px; min-width: 0; }
  /* Its own full-width line above the maps, and it keeps that line whether or not there is text: the
     global .readout:empty rule collapses an empty readout, which is right for a readout under a plot
     and wrong for one above two -- it made everything below jump on every mouse move onto a map. */
  .map-readout { flex: 1 1 100%; margin: 0; min-height: 15px; text-align: right; }
  .map-readout:empty { display: block; }
  /* Titles, because two unlabelled heatmaps side by side are a puzzle. Set in the same small caps as the
     card's section headings so the page has one voice for "this is what you are looking at". */
  .map-title {
    margin: 0 0 3px; font: 500 10px/1.4 var(--sans); letter-spacing: .12em;
    text-transform: uppercase; color: var(--ink-2);
  }
  .align-row a:hover { color: var(--cut); }
  .align-row.hi b { color: var(--cut); }

  footer { padding: 10px 18px 16px; border-top: 1px solid var(--rule); }
  footer p { margin: 0; font: 11px var(--mono); color: var(--ink-2); }
  /* The citations run to two lines: what does the encoding, and what was searched. */
  footer p + p { margin-top: 4px; }
  footer a { color: var(--bond); }

/* --- motion ------------------------------------------------------------------ */
  @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
