/* ButtFold. PLAN.md section 6: this is built to look like the Apple app, which is a
   requirement and not a preference. Every hex value below is taken from the live app by
   frequency of use, and --bf-stage was measured from a render rather than computed. */

:root {
  --bf-ground:        #0B0A1F;   /* page ground */
  --bf-ground-deep:   #05040E;   /* page gradient floor, footer */
  --bf-ground-raised: #181432;   /* cards, pills at rest */
  --bf-stage:         #0D0D26;   /* 3D stage clear colour: sRGB of the app's linear
                                    RGB (0.047, 0.039, 0.122), measured from a render */
  --bf-action:        #2B5CE6;   /* primary blue: buttons, selected pills */
  --bf-text-1:        #FFFFFF;
  --bf-text-2:        #6B7C93;   /* secondary text */
  --bf-text-3:        #B6BFD0;   /* dim text */
  --bf-text-4:        #8A93A8;   /* dimmer text */
  --bf-cyan:          #22E5FF;   /* accent; sheet on the ribbon */
  --bf-magenta:       #FF3D9A;   /* accent + error text; helix on the ribbon */
  --bf-amber:         #FCB900;   /* disclosure line, logo dot */
  --bf-green:         #3DDC97;   /* contacts trace; the one accent not already spoken for */
  --bf-pale-blue:     #8FB4FF;

  /* Ribbon colouring, the thing a viewer actually sees. Helix magenta, sheet cyan, coil
     slate, exactly as the app draws it. */
  --bf-helix:  var(--bf-magenta);
  --bf-sheet:  var(--bf-cyan);
  --bf-coil:   #6B7C93;

  --bf-radius: 12px;
  --bf-radius-pill: 999px;
  --bf-max: 1100px;

  /* Inter for UI, Roboto Mono for readouts and numbers: the closest web equivalents to the
     app's system font under the house rules. Apple's SF is not licensed for the web, so
     this is close rather than identical, and PLAN section 13 says so. */
  --bf-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --bf-mono: "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  color-scheme: dark;
}

/* The app is dark and only dark. A light variant would not be the app, and the
   resemblance is the requirement, so this commits to one look and paints it explicitly
   rather than borrowing the viewer's. */

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

body {
  background: linear-gradient(180deg, var(--bf-ground) 0%, var(--bf-ground) 60%,
                              var(--bf-ground-deep) 100%);
  background-attachment: fixed;
  color: var(--bf-text-3);
  font-family: var(--bf-sans);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--bf-pale-blue); text-decoration: none; }
a:hover { color: var(--bf-cyan); text-decoration: underline; }

.wrap { max-width: var(--bf-max); margin: 0 auto; padding: 0 20px; }

/* ---------------------------------------------------------------- the house header ---
   Structure is mandatory and comes from the marcs-vibe-coding skill: amber logo dot,
   "Marc C. Deller, D.Phil." linking to marcdeller.com, a contact link, sticky, never
   overlapping content. The same skill says the header takes the app's own palette rather
   than the default blue, so it sits on --bf-ground with --bf-action accents. The dot stays
   #FCB900, which happens to be both the skill's amber and PhoneFold's. */

.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(11, 10, 31, 0.86);
  backdrop-filter: saturate(160%) blur(12px);
  -webkit-backdrop-filter: saturate(160%) blur(12px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.site-header .wrap {
  display: flex; align-items: center; gap: 14px;
  min-height: 56px; flex-wrap: wrap;
}
.logo-dot {
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--bf-amber); flex: 0 0 auto;
  box-shadow: 0 0 12px rgba(252, 185, 0, 0.55);
}
.site-header .who { color: var(--bf-text-1); font-weight: 600; font-size: 15px; }
.site-header .who:hover { color: var(--bf-cyan); }
.site-header .spacer { flex: 1 1 auto; }
.site-header .header-link { color: var(--bf-text-2); font-size: 14px; }
.site-header .header-link:hover { color: var(--bf-cyan); }
.site-header .shop-link { color: var(--bf-action); font-weight: 600; font-size: 14px; }
.site-header .shop-link:hover { color: var(--bf-cyan); }

/* The header must stay one line. Measured at 390 px: the full shop-link wording wraps to
   three lines and pushes the protein name, the disclosure and the stage below the fold. */
.on-narrow { display: none; }
@media (max-width: 720px) {
  .on-wide { display: none; }
  .on-narrow { display: inline; }
  .site-header .wrap { gap: 10px; flex-wrap: nowrap; }
  .site-header .who { font-size: 14px; white-space: nowrap; }
  .site-header .header-link { display: none; }   /* the footer carries the contact link */
  .title-block { padding: 18px 0 10px; }
  .controls { gap: 6px 12px; }
  .control { flex: 1 1 100%; }
  .segmented { overflow-x: auto; max-width: 100%; }
}

/* ---------------------------------------------------------------- title block -------
   The protein name as a large title, and directly beneath it in amber the engine
   disclosure line. Exactly where the phone app puts it: in the frame, not in an About
   page. PLAN section 7. */

.title-block { padding: 16px 0 6px; }
.protein-name {
  font-size: clamp(26px, 4.2vw, 36px); line-height: 1.1;
  font-weight: 700; color: var(--bf-text-1); margin: 0;
  letter-spacing: -0.02em;
}
.protein-sub { color: var(--bf-text-2); font-size: 14px; margin: 4px 0 0; }
.disclosure {
  color: var(--bf-amber); font-size: 13.5px; margin: 6px 0 0;
  font-weight: 500;
}

/* ---------------------------------------------------------------- controls ------------
   One wrapping row rather than three stacked ones. Measured: three rows cost about 110 px
   and pushed the stage and the transport off a 900 px screen. The structure is the subject;
   the controls are not. */

.controls {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px 14px; margin: 10px 0 12px;
}
.control { display: flex; align-items: center; gap: 6px; }
.control > .label {
  color: var(--bf-text-2); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.06em; white-space: nowrap;
}
/* Five colour modes, five styles and three engines: thirteen buttons and three labels
   sharing one line at the 1100 px wrap. Measured, and asserted at four screen sizes by
   tests/stage_screenshot.mjs, because this is arithmetic over a dozen text widths and it is
   right when written and wrong the next time a label changes. */
.segmented {
  display: inline-flex; background: var(--bf-ground-raised);
  border-radius: var(--bf-radius-pill); padding: 3px; gap: 2px;
  border: 1px solid rgba(255, 255, 255, 0.05);
}
.segmented button {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  color: var(--bf-text-3); font: 500 12px/1 var(--bf-sans);
  padding: 7px 10px; border-radius: var(--bf-radius-pill);
  transition: background 120ms ease, color 120ms ease;
  white-space: nowrap;
}
.segmented button:hover:not([aria-pressed="true"]) { color: var(--bf-text-1); }
.segmented button[aria-pressed="true"] { background: var(--bf-action); color: #fff; }
.segmented button[disabled] { opacity: 0.4; cursor: not-allowed; }

/* The UniProt pulldown, on its own row and only in ESMFold mode. It is not in the control
   bar because that bar is 1060 px of the 1100 px it wraps at, measured, and a select in it
   would push the engine group onto a second line everywhere. */
.uniprot-row {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 12px; flex-wrap: wrap;
}
/* `display: flex` beats the browser's `[hidden] { display: none }`, so the row kept its
   height while hidden and pushed the readouts 42 px below the fold at three of the four
   screen sizes the layout gate checks. An attribute that means "not here" has to be said
   again by anything that sets display. */
.uniprot-row[hidden] { display: none; }
/* Measured, not added up: the row is 30 px of control plus 12 px of margin. */
body.picking-uniprot { --bf-page-chrome: 302px; }
.uniprot-row .label {
  color: var(--bf-text-2); font-size: 10.5px; text-transform: uppercase;
  letter-spacing: 0.06em; white-space: nowrap;
}
.uniprot-pick {
  appearance: none;
  background: var(--bf-ground-raised) url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'>\
<path d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%238b93b5' stroke-width='1.6'\
 stroke-linecap='round' stroke-linejoin='round'/></svg>") no-repeat right 12px center;
  background-size: 10px 7px;
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: var(--bf-radius-pill);
  color: var(--bf-text-1); font: 500 12px/1 var(--bf-sans);
  padding: 8px 32px 8px 14px; cursor: pointer; max-width: 100%;
}
.uniprot-pick:hover { border-color: rgba(255, 255, 255, 0.16); }
.uniprot-note {
  font: 500 11px/1.4 var(--bf-mono); color: var(--bf-text-2);
}
.uniprot-note:empty { display: none; }

/* ---------------------------------------------------------------- the viewer ----------
   The structure and its two time series, side by side. The charts used to sit below the
   readouts, where they added about 130 px and pushed the transport off a short screen; the
   stage is the subject, so it keeps roughly 60% of the width and the charts take the rest
   and stack. Below 900 px the two columns become one, because a 40% column of a phone is
   not a chart, it is a smear. */

.viewer {
  display: grid;
  grid-template-columns: minmax(0, 60fr) minmax(0, 40fr);
  gap: 14px;
  /* Stretch, not start: the LEFT column decides the row height - it is the stage, whose
     height is computed from the viewport - and the charts fill whatever that turns out to
     be. With `align-items: start` the two columns sized themselves independently and the
     charts came out 25 px taller than the stage at 1366x768, which put the transport back
     below the fold. */
  align-items: stretch;
}
.viewer-main { min-width: 0; }
.viewer-side { display: flex; flex-direction: column; min-height: 0; }

@media (max-width: 900px) {
  .viewer { grid-template-columns: 1fr; }
  .viewer-side { height: auto; }
}

/* ---------------------------------------------------------------- the stage ---------- */

.stage-wrap {
  position: relative; background: var(--bf-stage);
  border-radius: var(--bf-radius); overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.05);
  /* Sized from what is left after everything above and below it, so the transport is on
     screen without scrolling. The subtrahend is measured, not guessed: header 56, title
     block ~86, controls ~44, readouts ~52, transport ~56, plus margins. Measured in a real
     browser at four common sizes rather than added up on paper, and it has now been five
     pixels short TWICE at 1280x800 and 1366x768 - once at 330 and again at 252, arrived at
     by subtracting the height of the transport and the score line when they left the flow.
     Exactly the kind of arithmetic that is right when written and wrong three commits later,
     so tests/stage_screenshot.mjs measures where the readouts actually end rather than where
     they should. The ceiling rose to 620 because the transport is inside the frame now. The
     floor keeps a very short window usable by letting the page scroll rather than crushing
     the stage. */
  /* The subtrahend is a variable because the ESMFold engine adds a row above the stage:
     without this the pulldown pushed the readouts 42 px below the fold at three of the four
     sizes the layout gate checks, which is the same mistake as sizing the stage on paper. */
  height: clamp(260px, calc(100vh - var(--bf-page-chrome, 260px)), 620px);
  touch-action: none;   /* drag orbits the structure; it must not scroll the page */
}
/* On a phone the page scrolls anyway, so the stage takes a share of the viewport rather
   than what is left over: half of it, which is what the app gives it.

   This lived up with the rest of the phone block, three hundred lines ABOVE the rule it was
   meant to override. Same specificity, so the later one won and the phone had been using
   the desktop calculation the whole time - which is why a 844 px phone was giving the stage
   592 px rather than the 405 this asks for. A media query is not more specific than the rule
   it narrows. */
@media (max-width: 720px) {
  .stage-wrap { height: clamp(280px, 48vh, 460px); }
}
.stage-wrap canvas { display: block; width: 100%; height: 100%; }

/* The persistent engine badge. Small, in a corner, always visible while anything plays,
   and it never scrolls away while the animation runs. PLAN section 7. */
/* Full width along the stage's lower edge, under everything else in it. `pointer-events:
   none` because a drag that starts here has to reach the canvas behind it: the whole bottom
   strip of the viewer would otherwise be dead to the orbit. */
/* `.stage-wrap canvas` sizes three.js's canvas to fill the stage, and it matches THIS
   canvas too - at one class and one element it also outranks a bare `.residue-ribbon`, so
   the strip silently took the whole viewer. Scoped to the same weight to win it back rather
   than reaching for !important, and left specific about which canvas each rule means. */
.stage-wrap canvas.residue-ribbon {
  position: absolute; z-index: 1; left: 0; right: 0; bottom: 0;
  width: 100%; height: 34px; display: block; pointer-events: none;
}

/* One row along the bottom of the stage, holding the badge and the switch. Positioning both
   of them separately against the same corner worked until the stage was 350 px wide, where
   the badge wraps to two lines and grows underneath the switch. Layout does the spacing now,
   and `flex-wrap` puts the switch on its own line rather than on top of anything.

   It clears the ribbon while the strip is shown and drops to the stage's edge when it is
   not, so turning the music off leaves no empty band where the strip used to be. */
.stage-footer {
  position: absolute; z-index: 2; left: 12px; right: 12px; bottom: 42px;
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 8px; flex-wrap: wrap-reverse;
}

.music-toggle {
  display: inline-flex; align-items: center; gap: 8px; flex: none;
  padding: 6px 12px 6px 10px; border-radius: var(--bf-radius-pill);
  background: rgba(5, 4, 14, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  font: 500 11px/1 var(--bf-mono); color: var(--bf-text-3);
  letter-spacing: 0.04em; cursor: pointer; user-select: none;
  transition: color 140ms ease, border-color 140ms ease;
}
.music-toggle:hover { color: var(--bf-text-1); border-color: rgba(255, 255, 255, 0.2); }
.music-toggle input {
  appearance: none; -webkit-appearance: none;
  width: 13px; height: 13px; margin: 0; flex: none; cursor: pointer;
  border: 1.5px solid var(--bf-text-4); border-radius: 3px;
  background: transparent; position: relative;
  transition: background 140ms ease, border-color 140ms ease;
}
.music-toggle input:checked { background: var(--bf-green); border-color: var(--bf-green); }
.music-toggle input:checked::after {
  content: ''; position: absolute; left: 3.5px; top: 0.5px;
  width: 3px; height: 7px; border: solid var(--bf-ground);
  border-width: 0 1.8px 1.8px 0; transform: rotate(45deg);
}
.music-toggle input:focus-visible { outline: 2px solid var(--bf-action); outline-offset: 2px; }

/* Music off: the strip goes, and the row that was standing clear of it comes down. */
.stage-wrap.no-music canvas.residue-ribbon { display: none; }
.stage-wrap.no-music .stage-footer { bottom: 12px; }

.stage-badge {
  /* In the flow of the footer row now. `min-width: 0` so a long badge shrinks and wraps
     inside its own pill instead of pushing the switch off the stage. */
  min-width: 0; max-width: 100%;
  background: rgba(5, 4, 14, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--bf-radius-pill);
  padding: 6px 12px; font: 500 12px/1.3 var(--bf-mono);
  color: var(--bf-text-3);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.stage-badge .engine { color: var(--bf-amber); }
.stage-badge .where { color: var(--bf-text-2); }
/* What is happening, in the same line as what is producing it. Cyan so it reads as live
   rather than as chrome, and it disappears entirely when there is nothing to say. */
.stage-badge .status { color: var(--bf-cyan); }
.stage-badge .status:empty { display: none; }
.stage-badge .status::before { content: " \00B7  "; color: var(--bf-text-2); }
.stage-badge .status:empty::before { content: none; }


/* ---------------------------------------------------------------- readouts ------------ */

.readouts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  gap: 2px; margin: 2px 0 0;
  background: rgba(255, 255, 255, 0.04); border-radius: 0 0 var(--bf-radius) var(--bf-radius);
  overflow: hidden;
}
/* One row beside the stage, always. Two rows cost 57 px and put the transport back below
   the fold, so the labels are short enough to fit eight columns in a 60% one: the units are
   in the label and the values speak for themselves. */
@media (min-width: 901px) {
  .readouts { grid-template-columns: repeat(8, 1fr); }
  .readout { padding: 7px 8px; }
  .readout .k { font-size: 9.5px; letter-spacing: 0.04em; white-space: nowrap; }
}
/* Four by two on a phone, which is why there are eight of them rather than seven: an
   `auto-fit` grid gave three, three and one, and a row with a single figure in it reads as
   something having gone wrong rather than as a layout. */
@media (max-width: 900px) {
  .readouts { grid-template-columns: repeat(4, 1fr); }
}
.readout { background: var(--bf-ground); padding: 7px 12px; }
.readout .k {
  display: block; color: var(--bf-text-2); font-size: 10px;
  text-transform: uppercase; letter-spacing: 0.09em;
}
.readout .v {
  display: block; font-family: var(--bf-mono); font-size: 15px;
  color: var(--bf-text-1); font-variant-numeric: tabular-nums; margin-top: 2px;
}
.readout .v.helix { color: var(--bf-helix); }
.readout .v.sheet { color: var(--bf-sheet); }
.readout .v.coil  { color: var(--bf-coil); }

/* ---------------------------------------------------------------- charts -------------- */

/* Stacked, one above the other, filling the column beside the stage. */
.charts {
  display: grid; grid-template-rows: 1fr 1fr; gap: 12px; margin: 0;
  flex: 1 1 auto; min-height: 0;
}
/* Below 900 px the charts leave the stage's row, and with it the height they were flexing
   into. A canvas's flex basis is 0 with `min-height: 0` - deliberately, so it cannot feed
   its own height back into itself - so in an auto-height row it collapsed to nothing and
   both charts rendered as a title and a legend with a hairline between them. They need a
   row height of their own down here, which is the whole fix. */
@media (max-width: 900px) {
  .charts {
    grid-template-rows: none; grid-template-columns: 1fr 1fr;
    margin: 14px 0 0; flex: none;
  }
  .chart { height: 168px; }
}
@media (max-width: 620px) {
  .charts { grid-template-columns: 1fr; }
  .chart { height: 150px; }
}
.chart {
  background: var(--bf-ground-raised); border-radius: var(--bf-radius);
  padding: 10px 12px 8px; border: 1px solid rgba(255, 255, 255, 0.04);
  display: flex; flex-direction: column; min-height: 0; overflow: hidden;
}
.chart h3 {
  margin: 0 0 6px; font: 500 11px/1 var(--bf-sans); color: var(--bf-text-2);
  text-transform: uppercase; letter-spacing: 0.09em;
}
/* `flex: 1 1 0` with `min-height: 0`, NOT `1 1 auto` with a minimum. A canvas's intrinsic
   size comes from its width and height attributes, which `prepare()` in player.js sets from
   the element's own measured size - so an auto basis feeds the canvas's height back into
   itself and the chart column grows a little every layout. */
.chart canvas { display: block; width: 100%; flex: 1 1 0; min-height: 0; }
.chart .legend { font: 500 10px/1.4 var(--bf-mono); color: var(--bf-text-2); }
.chart .legend .h { color: var(--bf-helix); }
.chart .legend .e { color: var(--bf-sheet); }
.chart .legend .c { color: var(--bf-coil); }
.chart .legend .rg { color: #8FB4FF; }
.chart .legend .cp { color: var(--bf-amber); }
.chart .legend .ct { color: var(--bf-green); }
/* The legend now carries what the axis means, on hover. The smoothing window and the units
   used to be printed here; on Marc's instruction they came out to save the space, and a
   tooltip keeps the disclosure without spending a line on it. */
.chart .legend[title] { cursor: help; }

/* ---------------------------------------------------------------- transport -----------
   Inside the viewer, over the structure. Both of these used to sit in the page's flow
   under the stage, where they cost the structure a row of height apiece and put controls
   that only ever act on the viewer outside it. The stage's own pointer handlers are on the
   canvas, not on this wrapper, so a drag on a slider cannot also orbit the protein. */

/* Sized to match a `.segmented` group exactly, on Marc's instruction: 3px of padding
   around a 12px/1 control with 7px of its own, plus a 1px border, is 34px in both. The
   numbers are written here rather than shared through a variable because the two are read
   together on screen and a gate compares their measured heights. */
.stage-transport {
  position: absolute; z-index: 2; left: 12px; right: 12px; top: 12px;
  display: flex; align-items: center; gap: 10px;
  padding: 3px 12px 3px 3px;
  background: rgba(5, 4, 14, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--bf-radius-pill);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.play {
  appearance: none; border: 0; cursor: pointer;
  background: var(--bf-action); color: #fff;
  /* 600 rather than the segmented buttons' 500: same size, but this is the one control on
     the page you are meant to press first. */
  font: 600 12px/1 var(--bf-sans);
  padding: 7px 16px; border-radius: var(--bf-radius-pill);
  min-width: 78px; flex: none;
}
.play:hover { filter: brightness(1.12); }
.play:active { filter: brightness(0.94); }
.seek { flex: 1 1 auto; min-width: 60px; accent-color: var(--bf-action); }
.frame-count {
  font-family: var(--bf-mono); font-size: 11px; color: var(--bf-text-2);
  font-variant-numeric: tabular-nums; flex: none; white-space: nowrap;
}

/* Vertical, unlabelled, on the inside right.
   `writing-mode: vertical-lr` with `direction: rtl` is the standard way to turn a range
   input, and it puts the loud end at the top; `appearance: slider-vertical` is the older
   WebKit spelling and is kept for a Safari that does not honour the first. Both are
   declared because either alone leaves the control horizontal in one of the two engines,
   and a horizontal slider pinned to the right edge of the stage is 20 px wide and unusable
   rather than obviously broken. tests/stage_screenshot.mjs measures the box. */
.stage-volume {
  position: absolute; z-index: 2; right: 12px; top: 50%;
  transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  color: var(--bf-text-2);
  /* Dimmed to chrome and up to full strength under the pointer. It sits on the pair rather
     than on the input, so the mark and the slider cannot be at different strengths, and
     `focus-within` covers reaching it by keyboard. */
  opacity: 0.6; transition: opacity 0.15s ease;
}
.stage-volume:hover, .stage-volume:focus-within { opacity: 1; }
.volume-icon { display: block; flex: none; }

.volume {
  writing-mode: vertical-lr; direction: rtl;
  appearance: slider-vertical; -webkit-appearance: slider-vertical;
  width: 18px; height: clamp(90px, 22vh, 150px);
  accent-color: var(--bf-action);
  cursor: pointer; margin: 0;
}

.audio-note { margin: 4px 0 0; font-size: 13px; color: var(--bf-magenta); }
.audio-note:empty { display: none; }

/* ---------------------------------------------------------------- gallery ------------- */

.section-head {
  display: flex; align-items: baseline; gap: 12px; margin: 34px 0 12px;
}
.section-head h2 {
  margin: 0; font-size: 20px; color: var(--bf-text-1); font-weight: 650;
  letter-spacing: -0.01em;
}
.section-head .note { color: var(--bf-text-2); font-size: 13px; }

.gallery {
  display: flex; gap: 12px; overflow-x: auto; padding: 4px 0 14px;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}
.card {
  flex: 0 0 190px; scroll-snap-align: start;
  background: var(--bf-ground-raised); border-radius: var(--bf-radius);
  border: 1px solid rgba(255, 255, 255, 0.05);
  padding: 14px; cursor: pointer; text-align: left;
  appearance: none; color: inherit; font: inherit;
  transition: border-color 120ms ease, transform 120ms ease;
}
.card:hover { transform: translateY(-2px); border-color: rgba(255, 255, 255, 0.14); }
.card[aria-pressed="true"] { border-color: var(--bf-action); box-shadow: 0 0 0 1px var(--bf-action); }
.card .name { display: block; color: var(--bf-text-1); font-weight: 600; font-size: 15px; }
.card .sub {
  display: block; color: var(--bf-text-2); font-size: 12px; margin-top: 4px;
  font-family: var(--bf-mono);
}
.card .quality {
  display: block; color: var(--bf-text-4); font-size: 11px; margin-top: 8px;
  font-family: var(--bf-mono);
}

/* ---------------------------------------------------------------- honesty ------------- */

/* ---------------------------------------------------------------- the two panels ------
   One shape, two subjects: a line that cannot be missed, and the detail behind it. The
   amber rail is the app's disclosure colour and the cyan one the music's, which is the only
   thing that distinguishes them - they are the same object twice, not two designs. */
.panels { display: flex; flex-direction: column; gap: 10px; margin: 30px 0; }

.panel {
  background: var(--bf-ground-raised); border-radius: var(--bf-radius);
  border-left: 3px solid var(--bf-amber);
  color: var(--bf-text-3); font-size: 15px;
}
.panel-music { border-left-color: var(--bf-cyan); }

.panel > summary {
  padding: 16px 20px; cursor: pointer;
  display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
  /* Both spellings: the marker is a pseudo-element in WebKit and a list marker everywhere
     else, and the default triangle would sit in front of a paragraph rather than beside the
     control that actually says what opens. */
  list-style: none;
}
.panel > summary::-webkit-details-marker { display: none; }
.panel > summary::marker { content: ''; }
.panel > summary p { margin: 0; flex: 1 1 22em; min-width: 0; }
.panel > summary p strong { color: var(--bf-text-1); font-weight: 600; }
.panel:focus-within > summary { outline: none; }
.panel > summary:focus-visible {
  outline: 2px solid var(--bf-action); outline-offset: -2px; border-radius: var(--bf-radius);
}

/* The control. A chevron that turns, rather than a word that changes into another word in
   the same place, so the state is legible before you read anything. */
.panel .more {
  display: inline-flex; align-items: center; gap: 7px; flex: none;
  font: 500 11px/1 var(--bf-mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--bf-amber); white-space: nowrap;
  padding: 7px 12px 7px 13px; border-radius: var(--bf-radius-pill);
  border: 1px solid rgba(252, 185, 0, 0.28);
  transition: background 140ms ease, border-color 140ms ease;
}
.panel-music .more { color: var(--bf-cyan); border-color: rgba(34, 229, 255, 0.28); }
.panel > summary:hover .more { background: rgba(252, 185, 0, 0.10); border-color: rgba(252, 185, 0, 0.5); }
.panel-music > summary:hover .more { background: rgba(34, 229, 255, 0.10); border-color: rgba(34, 229, 255, 0.5); }
.panel .more::after {
  content: ''; width: 6px; height: 6px; margin-top: -3px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg); transition: transform 180ms ease;
}
.panel[open] .more::after { transform: rotate(-135deg); margin-top: 2px; }
.panel .more-shut, .panel[open] .more-open { display: none; }
.panel[open] .more-shut { display: inline; }

.panel .rest { padding: 0 20px 18px; }
.panel .rest p { margin: 0 0 12px; }
.panel .rest p:last-child { margin-bottom: 0; }

/* What each feature of the fold turns into. A two-column list rather than a table: it is
   pairs, not rows of independent values, and it collapses to one column on a phone without
   a horizontal scroll. */
.mapping {
  display: grid; grid-template-columns: minmax(0, 15em) minmax(0, 1fr);
  gap: 1px 0; margin: 0 0 14px;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.mapping dt, .mapping dd {
  margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.mapping dt { color: var(--bf-text-2); font-size: 13.5px; padding-right: 18px; }
.mapping dd { color: var(--bf-text-1); font-size: 13.5px; }
@media (max-width: 620px) {
  .mapping { grid-template-columns: 1fr; }
  .mapping dt { padding-bottom: 0; border-bottom: 0; font-size: 11px;
                text-transform: uppercase; letter-spacing: 0.07em; }
}

@media (prefers-reduced-motion: reduce) {
  .panel .more::after, .panel .more { transition: none; }
}

/* ---------------------------------------------------------------- shop window --------- */

.shop-feature {
  background: var(--bf-ground-raised); border-radius: var(--bf-radius);
  border: 1px solid rgba(255, 255, 255, 0.06);
  padding: 22px 24px; margin: 4px 0 8px;
}
.shop-feature h3 {
  margin: 0 0 8px; font-size: 22px; color: var(--bf-text-1); font-weight: 650;
  letter-spacing: -0.01em;
}
.shop-feature .blurb { margin: 0 0 12px; color: var(--bf-text-3); max-width: 60ch; }
.shop-feature .platforms {
  margin: 0 0 16px; font: 500 12px/1.5 var(--bf-mono); color: var(--bf-text-2);
}
.store-link {
  display: inline-block; background: var(--bf-action); color: #fff;
  font: 600 15px/1 var(--bf-sans); padding: 13px 24px;
  border-radius: var(--bf-radius-pill); text-decoration: none;
}
.store-link:hover { filter: brightness(1.12); color: #fff; text-decoration: none; }

/* Not dimmed. An app in review is a fact about the app, not a disabled state, and greying
   it out would read as "broken" rather than as "not yet". */
.status-tag {
  display: inline-block; margin: 0 0 10px;
  background: rgba(252, 185, 0, 0.14); color: var(--bf-amber);
  border: 1px solid rgba(252, 185, 0, 0.32);
  border-radius: var(--bf-radius-pill); padding: 8px 16px;
  font: 500 13px/1 var(--bf-sans);
}
.fallback-link { display: block; font-size: 14px; }

.card-static { cursor: default; }
.card-static:hover { transform: none; border-color: rgba(255, 255, 255, 0.05); }
.card .blurb-small {
  display: block; color: var(--bf-text-4); font-size: 12px; margin-top: 8px;
  line-height: 1.45;
}
.gallery .card { flex: 0 0 220px; }

/* ---------------------------------------------------------------- footer -------------- */

.site-footer {
  margin-top: 42px; padding: 26px 0 40px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--bf-text-2); font-size: 13px;
}
/* The joke carries the emphasis, on Marc's instruction. It was `.honesty .joke`, and the
   block stopped being `.honesty` when it became a panel - so this had been styling nothing
   at all and the line was rendering as plain body text. A selector scoped to a class that
   no longer exists fails silently and looks like a design choice. */
.panel .joke { color: var(--bf-text-1); font-weight: 600; }

/* ---------------------------------------------------------------- utilities ----------- */

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

.no-wasm-note {
  display: none; color: var(--bf-magenta); font-size: 13px; margin-top: 8px;
}
body.no-wasm .no-wasm-note { display: block; }

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