/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

/* Rendered Markdown (a module's description). */
.markdown-body { word-wrap: break-word; }
.markdown-body h1, .markdown-body h2, .markdown-body h3 { color: var(--pulsers-text); font-weight: 700; margin: 12px 0 6px; }
.markdown-body h1 { font-size: 18px; }
.markdown-body h2 { font-size: 16px; }
.markdown-body h3 { font-size: 14px; }
.markdown-body p { margin: 0 0 10px; }
.markdown-body ul, .markdown-body ol { margin: 0 0 10px; padding-left: 20px; }
.markdown-body li { margin: 2px 0; }
.markdown-body a { color: var(--pulsers-accent); text-decoration: underline; }
.markdown-body code { background: var(--pulsers-surface); border: 1px solid var(--pulsers-line); border-radius: 3px; padding: 1px 4px; font-family: var(--font-mono); font-size: 0.9em; }
.markdown-body blockquote { border-left: 3px solid var(--pulsers-line); margin: 0 0 10px; padding-left: 12px; color: var(--pulsers-muted); }

/* ── What the pointer says about an element ────────────────────────────────

   A <button> ships with `cursor: default` in every browser, so anything that
   was not handed a class said nothing under the pointer — 38 of ours, from the
   panel section arrows to the account menu. Stated once here rather than as a
   utility repeated on each tag, which is how half of them came to be missed.

   Disabled controls say so too: the project already draws them greyed
   (ButtonComponent, the announced-synth cards), and the pointer now agrees. */
button:not(:disabled),
summary,
[role="button"]:not([aria-disabled="true"]),
label:has(input[type="checkbox"]),
label:has(input[type="radio"]) {
  cursor: pointer;
}

button:disabled,
[aria-disabled="true"] {
  cursor: not-allowed;
}

/* ── Realistic rack chrome (rails, screw holes, HP ruler) — read-only shows ── */
.rack-body { position: relative; padding: 7px 0; border-radius: 4px; background-color: #161616; }

/* Brushed-metal rails top & bottom, with screw holes punched every 1 HP. The
   holes are a single repeating radial-gradient, not one element per screw. */
.rack-rail {
  position: relative;
  height: 7px;
  background:
    radial-gradient(circle at calc(var(--hp-px) / 2) 50%, #0b0b0b 0 1.1px, transparent 1.4px) repeat-x,
    linear-gradient(180deg, #3d3d3d 0%, #262626 45%, #1a1a1a 100%);
  background-size: var(--hp-px) 100%, auto;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07), inset 0 -1px 0 rgba(0,0,0,.6);
}
.rack-rail--top { border-radius: 3px 3px 0 0; margin-bottom: 2px; }
.rack-rail--bottom { border-radius: 0 0 3px 3px; margin-top: 2px; }

/* Aluminium HP ruler. */
.rack-ruler { background: linear-gradient(180deg, #2a2a2a, #202020); border-radius: 3px 3px 0 0; overflow: hidden; margin-bottom: 2px; }
.rack-ruler-tick { position: absolute; top: 0; width: 1px; height: 6px; background: #4a4a4a; }
.rack-ruler-tick--major { height: 11px; background: #6b7f88; }
.rack-ruler-num { position: absolute; bottom: 1px; transform: translateX(2px); font-family: 'IBM Plex Mono', monospace; font-size: 9px; color: #8a9aa0; }

/* Module link + hover "View ↗" overlay (show rack only). */
.rack-module-link { cursor: pointer; }
.rack-module-overlay {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 4px;
  background: rgba(13,13,13,.55); border: 1.5px solid var(--pulsers-accent); border-radius: 4px;
  box-shadow: 0 0 12px color-mix(in srgb, var(--pulsers-accent) 35%, transparent);
  opacity: 0; transition: opacity 120ms ease; pointer-events: none;
}
.rack-module-link:hover .rack-module-overlay,
.rack-module-link:focus-visible .rack-module-overlay { opacity: 1; }
.rack-module-overlay-label { font-family: var(--font-mono); font-size: 11px; font-weight: 700; color: var(--pulsers-accent); }
.rack-module-overlay-name { font-family: var(--font-mono); font-size: 9px; color: var(--pulsers-text); text-align: center; padding: 0 4px; }

/* Read-only rack/patch shows: components are not draggable, so no move cursor.
   Overrides the editor's `.placed-component { cursor: move }` by specificity. */
.rack-body .placed-component,
.rack-body .placed-component * { cursor: default; }

/* Knob/slider ViewComponents set `cursor:grab` inline, from the days when a
   panel could be dragged about. Nothing drags one now, so force the default
   cursor. Scoped !important is required to beat the inline style. */
.rack-body .placed-component svg { cursor: default !important; }

/* ── Module show: panel | characteristics, then the rest under the panel ───── */
/* Two rows on purpose. The panel shares row 1 with the characteristics card so
   the two blocks align top and bottom; the caption and the description sit in
   row 2, in the panel's column. Nesting them with the panel would have made the
   left column taller than the card, and the two would never line up. */
.module-show-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 288px;
  grid-template-rows: auto auto;
  gap: 24px;
  margin-bottom: 40px;
}
.module-show-panel { grid-column: 1; grid-row: 1; }
.module-show-cols > .module-specs { grid-column: 2; grid-row: 1; align-self: start; }
.module-show-under-panel { grid-column: 1; grid-row: 2; }

@media (max-width: 1000px) {
  .module-show-cols { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, the description would push the figures far down the page, so the
     card comes first — the numbers are what a reader came for. */
  .module-show-cols > .module-specs { grid-column: 1; grid-row: 2; }
  .module-show-under-panel { grid-row: 3; }
}

/* What matters is that this block, not the module, is what holds a width: it
   always fills its column, and the module is centred on it whatever its size.
   Tying the layout to the panel's width is what went wrong twice — a fixed
   column clipped the wide panels, a flexible one left a 4 HP module floating in
   570px of nothing. min-width:0 lets a wide panel scroll rather than stretch. */
.module-show-panel { min-width: 0; }

/* The frame takes the row's height and aligns with the characteristics card; the
   module is laid inside it at the scale the server rendered it (3.6 px/mm) and
   centred, never stretched.

   Scaling the SVG to the row is what this used to do, and it was wrong twice
   over: it showed the module at a size it does not have, and it showed it
   differently from the editor — where the panel is traced against the reference
   photo. A 3U module is always 128.5mm tall, so the gap to fill was a constant
   30px, and deforming every panel to close it bought nothing.

   Centring is `align-items`, not an auto margin: the partial writes
   `margin: 0 auto` inline for the horizontal axis, which an outside rule cannot
   override — but zero vertical margins leave align-items free to do its work. */
.module-show-panel .module-panel {
  height: 100%;
  align-items: center;
  padding: 13px;
  background-color: var(--pulsers-thumb-bg);
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
}

/* Millimetres, because that is the measurement that says whether the module
   fits a rack. The scroll hint is hidden until the panel actually overflows —
   announcing a gesture that does nothing is worse than not announcing it. */
.module-show-panel-hint {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin: 10px 0 0;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--pulsers-muted);
}
.module-show-panel-scroll { display: none; color: var(--pulsers-accent); }
/* The attribute sits on the scroller inside the panel block, and the hint is
   further down the grid — `~`, not `+`: making the panel and the card share a
   row moved the caption into its own grid item, so the two stopped being
   adjacent siblings and the hint silently never showed again. */
.module-show-panel:has(.module-panel[data-overflow-start]) ~ .module-show-under-panel .module-show-panel-scroll,
.module-show-panel:has(.module-panel[data-overflow-end]) ~ .module-show-under-panel .module-show-panel-scroll {
  display: inline;
}

/* ── Module panel: scroll affordance ───────────────────────────────────────────
   A wide panel scrolls rather than being clipped, but the native scrollbar stays
   hidden on macOS until the user scrolls, so a panel wider than its column just
   looked cut. The edge fades away while content remains on that side; the
   panel-overflow controller sets the data attributes, since CSS alone can't tell
   whether anything is hidden. Fades only, no layout shift. */
.module-panel[data-overflow-end] {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 56px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 56px), transparent);
}
.module-panel[data-overflow-start] {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 56px);
  mask-image: linear-gradient(to right, transparent, #000 56px);
}
.module-panel[data-overflow-start][data-overflow-end] {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 56px, #000 calc(100% - 56px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 56px, #000 calc(100% - 56px), transparent);
}

/* ── Demo thumbnails grid ──────────────────────────────────────────────────── */
/* Two big tiles per row rather than a strip of stamps: a demo is the point of
   the page for a reader deciding whether they want the module. minmax() gets
   there without a breakpoint — one column when the room for two is gone. */
.demo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 16px; }

/* The stage holds the thumbnail and, once playing, the iframe that replaces it.
   Its ratio is fixed so starting a demo does not shift the rest of the page. */
.demo-tile-stage { position: relative; width: 100%; aspect-ratio: 16 / 9; }
.demo-tile-stage--audio { aspect-ratio: auto; height: 166px; }
.demo-tile-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; border-radius: 6px; }
.demo-tile-stage--audio .demo-tile-frame { position: static; height: 166px; }
.demo-tile-stage[data-playing] .demo-tile { display: none; }
.demo-tile {
  position: absolute; inset: 0; display: block; width: 100%; height: 100%;
  padding: 0; border: 1px solid var(--pulsers-line); border-radius: 6px; overflow: hidden;
  background: var(--pulsers-thumb-bg); cursor: pointer; transition: border-color 120ms ease;
}
.demo-tile:hover, .demo-tile:focus-visible { border-color: var(--pulsers-accent); }
.demo-tile-thumb { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.demo-tile-fallback {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: 13px; letter-spacing: 0.14em; color: var(--pulsers-muted);
}
.demo-tile-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
.demo-tile-figure { margin: 0; }
.demo-tile-caption {
  margin-top: 8px; color: var(--pulsers-muted); font-family: var(--font-mono); font-size: 12px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Toolbar: theme and locale, same control in every layout ───────────────── */
.toolbar { display: flex; align-items: center; gap: 8px; }
.toolbar-button {
  display: inline-flex; align-items: center; gap: 7px;
  height: 30px; padding: 0 12px;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; white-space: nowrap;
  color: var(--pulsers-muted);
  background: var(--pulsers-surface);
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.toolbar-button:hover { color: var(--pulsers-text); border-color: var(--pulsers-faint); }
.toolbar-button:focus-visible { outline: 2px solid var(--pulsers-accent); outline-offset: 2px; }
/* Every control in the bar is one of these, so they share a single height. The
   user button used to be a bare 20px icon next to 30px buttons, which is what
   made the row look misaligned. */
a.toolbar-button { text-decoration: none; }
.toolbar-button--accent {
  color: var(--pulsers-on-accent);
  background: var(--pulsers-accent);
  border-color: var(--pulsers-accent);
  font-weight: 700;
}
.toolbar-button--accent:hover { color: var(--pulsers-on-accent); border-color: var(--pulsers-accent); }

/* The burger is mobile-only. Tailwind's `md:hidden` would fight `.toolbar-button`
   over `display` and win or lose depending on which sheet loads last, so the
   rule lives here where there is nothing to arbitrate. */
.toolbar-button--burger { display: none; }
@media (max-width: 767px) { .toolbar-button--burger { display: inline-flex; } }

/* ── Rack listing: one row per rack ────────────────────────────────────────── */
/* The row wraps rather than squeezing: below its threshold the thumbnail and the
   identity stay on the first line and the figures drop underneath, instead of
   the name being crushed between two blocks — which is what it did on a phone. */
.rack-row {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 13px 15px;
  background: var(--pulsers-surface);
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  text-decoration: none;
  transition: border-color 120ms ease;
}
.rack-row:hover { border-color: var(--pulsers-accent); }

/* Framed, so an empty rack still reads as a rack rather than as blank space. */
.rack-row-thumb {
  flex: 0 0 auto; width: 142px; overflow: hidden;
  background: var(--pulsers-thumb-bg);
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  padding: 5px;
}

/* How full the rack is — the figure that decides whether another module fits,
   and which the listing showed nowhere although the database held every part of
   it. The dot carries the same reading as the colour, for anyone who cannot use
   the colour alone. */
.rack-row-fill {
  display: inline-flex; align-items: center; gap: 6px;
  margin: 4px 0 0;
  font-family: var(--font-mono); font-size: 11px;
  color: var(--pulsers-ok);
}
.rack-row-fill::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: 0 0 auto;
}
.rack-row-fill--full { color: var(--pulsers-warn); }

/* The same proportion as a length, read without doing the subtraction. */
.rack-row-meter {
  display: block; height: 4px; width: 126px; margin: 6px 0 0 auto;
  background: var(--pulsers-line); border-radius: 1px; overflow: hidden;
}
/* Filled in the nominal colour, not in the ignition one. How full a rack is
   is a measured value, and the accent marks what triggers or is selected — it
   never qualifies a figure. */
.rack-row-meter i { display: block; height: 100%; background: var(--pulsers-ok); }

@media (max-width: 640px) {
  .rack-row > div:last-child { flex: 1 1 100%; text-align: left; }
  .rack-row-meter { margin-left: 0; }
}

/* ── Synth listing: what you can play, then what is coming ─────────────────── */
/* Contact form — the reason chips.

   Same shape and same values as FilterChipComponent, which the listings use to
   filter. That one is a link, because filtering is a GET whose state belongs in
   the URL; these are buttons, because choosing a reason navigates nowhere and
   only rewrites the question below. Written here rather than as Tailwind classes
   so the controller can toggle one modifier instead of swapping six utilities. */
.contact-chip {
  font-family: var(--font-mono); font-size: 12px;
  padding: 6px 12px;
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  background: var(--pulsers-surface);
  color: var(--pulsers-muted);
  cursor: pointer;
  transition: border-color .15s, color .15s;
}
.contact-chip:hover { border-color: var(--pulsers-accent); color: var(--pulsers-text); }
.contact-chip:focus-visible { outline: 2px solid var(--pulsers-accent); outline-offset: 2px; }
.contact-chip--on {
  background: var(--pulsers-accent);
  border-color: var(--pulsers-accent);
  color: var(--pulsers-on-accent);
}
.contact-chip--on:hover { color: var(--pulsers-on-accent); }

/* Panel view switch — the two ways to look at an instrument.

   Laid over the panel's bottom right rather than added to the toolbar: it acts
   on the drawing under it, and the toolbar already carries everything that acts
   on the patch. Two segments rather than one button that renames itself, so the
   state you are in and the one you can reach are both readable without clicking.

   Same values as .contact-chip, one radius for the pair. */
/* Under the panel it acts on, and never out of reach. Two earlier placements
   were wrong in opposite ways: fixed to the window it floated across the whole
   document, following the reader down into content it has nothing to do with;
   docked in the panel's bottom corner it covered whatever controls happened to
   be there. It rides at the end of the panel wrapper's column instead, so on a
   panel that fits it takes its own room and hides nothing.

   Sticky for the panels that do not fit: at actual size a Lyra-8 runs past the
   bottom of the window and took the switch down to 1405px with it, so the one
   control that shrinks the panel back was the one you had to scroll to find. It
   holds 16px off the bottom until the panel ends, covering a strip of artwork
   only while there is artwork below it to go back to. */
.panel-fit-dock {
  position: sticky;
  bottom: 16px;
  align-self: flex-end;
  z-index: 40;
}

.panel-fit {
  display: inline-flex;
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  overflow: hidden;
  background: var(--pulsers-surface);
  /* The panel behind can be any colour a machine ships in, so the group carries
     its own separation from it. */
  box-shadow: 0 1px 3px rgba(0, 0, 0, .28);
}
.panel-fit__option {
  font-family: var(--font-mono); font-size: 12px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px;
  border: 0;
  background: transparent;
  color: var(--pulsers-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: background-color .15s, color .15s;
}
.panel-fit__option + .panel-fit__option { border-left: 1px solid var(--pulsers-line); }
.panel-fit__option:hover { color: var(--pulsers-text); }
.panel-fit__option:focus-visible { outline: 2px solid var(--pulsers-accent); outline-offset: -2px; }
.panel-fit__option--on {
  background: var(--pulsers-accent);
  color: var(--pulsers-on-accent);
}
.panel-fit__option--on:hover { color: var(--pulsers-on-accent); }
.panel-fit__icon { width: 13px; height: 13px; flex: none; }

/* Field shapes, shared by every form on the site. The forms used to spell these
   out inline, each with its own radius — 4px on the rack and module creation
   pages, 6px elsewhere — while the system has one, at 2px. */
.form-field {
  width: 100%;
  background: var(--pulsers-field-bg);
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  color: var(--pulsers-text);
  font-family: var(--font-mono);
  font-size: 13px;
  padding: 9px 11px;
}
.form-field:focus { outline: none; border-color: var(--pulsers-accent); }
.form-field--multiline { min-height: 150px; resize: vertical; line-height: 1.55; }
/* A field holding two digits should not span the page. Declared here rather
   than with a `w-28` utility, which loses to `.form-field { width: 100% }` —
   same specificity, and this file is loaded last. */
.form-field--narrow { width: 7rem; }

/* Same rule-to-the-right label as the catalogue's result count: it separates two
   groups without a heading that would compete with the page title. */
/* Written as words, in the reading face: "Avec patchbay" is a sentence about the
   instruments below it, not a legend silkscreened on a machine. Spaced capitals
   in a monospace made every label on the site sound like a warning plate. */
.synths-label {
  display: flex; align-items: center; gap: 14px; margin: 0 0 16px;
  font-size: 15px; color: var(--pulsers-muted);
}
.synths-label::after { content: ""; flex: 1; height: 1px; background: var(--pulsers-hair); }

/* The instruments in a section, laid out in columns rather than on a grid.

   Their artwork does not share a shape: the Matriarch's is 1200×262 and the
   Pulsar-23's is nearly square, so a card can be 185 tall beside one of 408. A
   grid gives every card in a row the height of the tallest, which put 224 units
   of empty ground under the Matriarch and 272 under the Polivoks — the two
   sections carried 496 between them.

   Columns fill independently and the browser balances them, so the empty ground
   goes. What changes is the reading order: down one column, then the next. The
   sections are sorted by patch points, and that survives it — each column stays
   in order and the second picks up where the first left off.

   break-inside keeps a card whole; a card split across the fold would show its
   artwork in one column and its name in the other. */
.synths-columns {
  columns: 1;
  column-gap: 24px;
  /* 24 under the last card of each column, so 16 here makes the 40 that
     separates one section from the next. */
  margin: 0 0 16px;
}
@media (min-width: 768px) {
  .synths-columns { columns: 2; }
}
.synths-columns > * {
  break-inside: avoid;
  margin-bottom: 24px;
}

/* The widest panel takes both columns. At half a column the Matriarch's 4.58:1
   artwork comes to 102 units of height and its knobs are specks; across the
   full width it is 210. See WIDE_CARD_RATIO for why the threshold falls where
   it does.

   column-span cuts the column flow where it lands, so whatever precedes it
   forms its own short block. The sections are ordered by patch points and the
   Matriarch is second, which leaves the Pulsar-23 alone above it with empty
   ground to its right. That is the cost of keeping the measured order, and it
   was preferred to moving the instrument to the top of its section. */
.synths-columns-wide { column-span: all; }

/* The break that column-span makes swallows the margin under whatever precedes
   it: the card above ended flush against this one, measured at zero. Put the
   gap back on this side. Not on a first child, where it would push the whole
   section down instead. */
.synths-columns-wide:not(:first-child) { margin-top: 24px; }

/* A maker's name over its instruments. Quieter than the section label above it
   — that one separates usable from announced, this one only groups — so it
   carries no rule and sits at body weight. */
.synths-brand {
  margin: 0 0 10px;
  font-family: var(--font-mono); font-size: 12px;
  letter-spacing: 0.08em; color: var(--pulsers-body);
}

.synths-announced {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 8px;
}
/* An announced synth has a page too — it states what the instrument is and that
   its panel is not drawn yet. The row is the frame, the link inside it is what
   fills and carries the layout, so the whole tile is clickable. */
.synths-announced li {
  background: var(--pulsers-surface);
  border: 1px solid var(--pulsers-line);
  border-radius: 2px;
  transition: border-color .2s;
}
.synths-announced li:hover { border-color: var(--pulsers-accent); }
.synths-announced-link {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 13px;
  color: inherit; text-decoration: none;
}
.synths-announced-name { display: block; font-family: var(--font-mono); font-size: 12px; font-weight: 700; }
.synths-announced-brand { display: block; font-family: var(--font-mono); font-size: 11px; color: var(--pulsers-muted); }
.synths-announced-state {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--pulsers-faint);
}

/* Module show — header & body text. Every colour here is a token: the page used
   to paint itself var(--pulsers-ground) on var(--pulsers-text) whatever the theme, which is why the
   characteristics card went white while its values stayed near-white on it. */
.module-show { background-color: var(--pulsers-ground); color: var(--pulsers-text); }
.module-show-inner { max-width: 1080px; margin: 0 auto; padding: 24px 16px; }
/* Graph paper under the content: this is a page about a physical object with
   millimetre dimensions, and the grid says so without taking any room. One
   container carries it, so the ruling stays aligned across the whole body — set
   per block, each background would restart at its own origin. The header stays
   on plain ground above it. */
.module-show-body,
.module-index-body {
  padding: 22px;
  background-image:
    linear-gradient(to right, var(--pulsers-grid) 1px, transparent 1px),
    linear-gradient(to bottom, var(--pulsers-grid) 1px, transparent 1px);
  background-size: 22px 22px;
}
/* The catalogue stacks its own blocks; the module page positions its two
   columns itself, so only this one gets a gap. */
/* minmax(0, 1fr), not the implicit `auto`: an undeclared grid track sizes itself
   to its widest content, and a grid item defaults to min-width: auto, so a
   single select wide enough to hold "Toutes les fonctions" stretched the track —
   and with it the rule, the scale board and every row — 370px past a phone
   viewport. The track now refuses to grow beyond its container. */
.module-index-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px; }

/* A count with a rule running off to the right: it separates the controls from
   the results without a heading that would compete with the page title. */
.module-index-results {
  display: flex; align-items: center; gap: 14px; margin: 0 0 -8px;
  font-size: 15px; color: var(--pulsers-muted);
}
.module-index-results::after { content: ""; flex: 1; height: 1px; background: var(--pulsers-hair); }

/* ─── Home, screen by screen ───────────────────────────────────────────────
   One section per object. The product is the hero and the page is the pedestal:
   the object sits on the ground with no card, no shadow and no frame, the title
   is centred above it, and the figures go underneath — a count only means
   something next to the thing it counts.

   Sections are told apart by the value of their ground, never by a rule: a
   separator would add one more object to a page that deliberately shows one. */

.screen { padding: 96px 0 104px; text-align: center; }
.screen--deep { background: var(--pulsers-void); }
.screen--mid { background: var(--pulsers-surface); }

/* 980px, as the reference holds it: it is what makes a headline read as one
   block rather than as a banner running the width of the window. */
.screen-stage { max-width: 980px; margin: 0 auto; padding: 0 24px; }

.screen-eyebrow {
  font-size: 15px; color: var(--pulsers-muted); margin: 0 0 14px;
}

/* Line height 1.07 is the signature: a two-line headline sits as a single
   visual mass instead of two stacked sentences. */
.screen-title {
  font-size: clamp(38px, 6vw, 70px);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 0.95;
  margin: 0 0 18px;
  text-wrap: balance;
  color: var(--pulsers-text);
}

.screen-sub {
  font-size: 21px; line-height: 1.38; letter-spacing: 0.011em;
  color: var(--pulsers-muted); margin: 0 auto 26px; max-width: 34ch;
}

.screen-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

/* No card, no shadow, no padding around it. */
.screen-object { margin-top: 46px; }

.screen-plate {
  display: block; width: 100%;
  background-size: contain; background-repeat: no-repeat; background-position: center;
  outline: 1px solid var(--pulsers-panel-outline);
}

/* Aligned at the top, not the bottom: every 3U face renders to the same height,
   so the tops line up and the bottoms follow. Bottom-aligning the figures made
   a face jump whenever its caption wrapped onto a second line. */
.screen-pair { display: flex; gap: 26px; align-items: flex-start; justify-content: center; }
.screen-pair figure { margin: 0; }
.screen-pair figcaption {
  margin-top: 12px; font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.042em; text-transform: uppercase;
  color: var(--pulsers-muted); font-variant-numeric: tabular-nums;
}
.screen-pair figcaption b { color: var(--pulsers-text); font-weight: 400; }

.screen-readout {
  margin: 18px 0 0; display: flex; gap: 22px; justify-content: center; flex-wrap: wrap;
  font-family: var(--font-mono); font-size: 11px;
  letter-spacing: 0.042em; text-transform: uppercase;
  color: var(--pulsers-muted); font-variant-numeric: tabular-nums;
}
.screen-readout b { color: var(--pulsers-text); font-weight: 400; }

@media (max-width: 640px) {
  .screen { padding: 64px 0 72px; }
  .screen-pair { gap: 14px; }
}

/* Section labels inside the listing, same voice as the results count. */
.module-section-label {
  display: flex; align-items: baseline; gap: 14px; margin: 0 0 8px;
  font-size: 20px; font-weight: 600; letter-spacing: -0.015em;
  color: var(--pulsers-text);
}
.module-section-label::after { content: ""; flex: 1; height: 1px; background: var(--pulsers-hair); }
.module-section-note {
  margin: 0 0 22px; font-size: 15px;
  color: var(--pulsers-muted); max-width: 78ch;
}

/* The scale board. Panels are laid on the deepest value of the palette so a
   face of any colour detaches from it — they carry their own body colour and
   follow no theme. */
.scale-board {
  background: var(--pulsers-void);
  border-radius: var(--radius-panel);
  padding: 12px;
  display: flex; flex-direction: column; gap: 10px;
  box-shadow: var(--pulsers-elevation);
}
/* Drawn on every rendered face, without exception: the Mother-32 is black and
   would lose its edge on this ground. */
.scale-face svg { outline: 1px solid var(--pulsers-panel-outline); }
/* What is left of a row, hatched so it reads as space rather than as an object.
   Hatched in the line colour, not in the accent wash: empty width is not an
   action and the ignition colour has no business marking it. */
.free-rail {
  background: repeating-linear-gradient(
    90deg, transparent 0 9px, var(--pulsers-line) 9px 10px);
}

/* Specification table. Figures line up in columns, so they are tabular and the
   numeric cells are right-aligned; an absent one is named in the warning colour
   rather than left blank, where it would read as a zero. */
/* Column heads name things — "Module", "Marque", "Profondeur" — so they take the
   reading face. The cells under them hold measurements and keep the monospace. */
.spec-th {
  padding: 13px 16px; background: var(--pulsers-void);
  border-bottom: 1px solid var(--pulsers-line);
  font-size: 14px; font-weight: 500;
  color: var(--pulsers-muted);
  white-space: nowrap;
}
.spec-td {
  padding: 12px 16px; border-bottom: 1px solid var(--pulsers-hair);
  font-size: 14px; white-space: nowrap; color: var(--pulsers-text);
}
.spec-row:last-child .spec-td { border-bottom: none; }
.spec-row:hover .spec-td { background: var(--pulsers-raised); }
.spec-num { font-family: var(--font-mono); font-size: 13px; font-variant-numeric: tabular-nums; text-align: right; }
/* Lower case and normal tracking: set in spaced capitals like the other labels,
   this one ran wide enough to push the price column out of the table. */
.spec-absent {
  font-family: var(--font-mono); font-size: 12px;
  letter-spacing: 0; color: var(--pulsers-warn);
}
.spec-foot {
  padding: 13px 16px; background: var(--pulsers-void);
  border-top: 1px solid var(--pulsers-line);
  font-size: 14px; color: var(--pulsers-muted);
  font-variant-numeric: tabular-nums;
}
.spec-foot.spec-num { color: var(--pulsers-text); font-size: 13px; }
/* The header carries the same 22px inset as the body below it, so the title and
   the panel frame line up while the ruled area still runs to the container's
   edge — the grid needs room around the content, not around the page. */
.module-show-header {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 20px; flex-wrap: wrap;
  padding: 4px 22px 18px;
  border-bottom: 1px solid var(--pulsers-line);
}
.module-show-name {
  font-family: var(--font-mono); color: var(--pulsers-text);
  font-size: 20px; font-weight: 700; letter-spacing: 0.05em; margin: 0 0 5px;
}
.module-show-meta { font-family: var(--font-mono); color: var(--pulsers-muted); font-size: 12px; margin: 0; }
.module-show-edit {
  font-family: var(--font-mono); font-size: 11px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase;
  background: var(--pulsers-accent); color: var(--pulsers-on-accent);
  border-radius: 2px; padding: 8px 16px; text-decoration: none; white-space: nowrap;
}
.module-show-h2 { font-family: var(--font-mono); color: var(--pulsers-text); font-size: 12px; text-transform: uppercase; letter-spacing: 0.16em; margin: 0; }
.module-show-desc-block { margin-top: 22px; }
.module-show-desc-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.module-show-fallback-badge {
  font-family: var(--font-mono); font-size: 11px;
  color: var(--pulsers-warn); border: 1px solid var(--pulsers-warn);
  border-radius: 2px; padding: 2px 8px;
}
.module-show-desc { color: var(--pulsers-muted); font-size: 13.5px; line-height: 1.7; max-width: 66ch; }
.module-show-manufacturer { margin: 14px 0 0; }
.module-show-link { color: var(--pulsers-accent); font-family: var(--font-mono); font-size: 12px; text-decoration: none; }
.module-show-link:hover { text-decoration: underline; }
.module-show-demos { margin-top: 32px; }
.demo-link { color: var(--pulsers-accent); font-family: var(--font-mono); font-size: 13px; text-decoration: none; word-break: break-all; }

/* ── Shared app modal (module show opened from a rack via turbo-frame) ──────── */
/* The overlay never scrolls the page: it centres a height-capped dialog whose
   OWN content scrolls when it overflows. */
.app-modal-overlay {
  position: fixed; inset: 0; z-index: 90; display: none;
  align-items: center; justify-content: center; padding: 24px 16px;
  background: rgba(0,0,0,.78);
}
.app-modal-overlay.is-open { display: flex; }
.app-modal-dialog {
  position: relative; width: 100%; max-width: 1120px;
  max-height: calc(100vh - 48px); overflow: hidden;
  display: flex; flex-direction: column;
  background: var(--pulsers-ground); border: 1px solid var(--pulsers-line); border-radius: 10px;
}
.app-modal-content { overflow-y: auto; }
.app-modal-close {
  position: absolute; top: 12px; right: 14px; z-index: 2;
  background: transparent; border: none; color: var(--pulsers-muted); font-size: 18px; cursor: pointer; line-height: 1;
}
.app-modal-close:hover { color: var(--pulsers-text); }
