ommp 0.1.3

Oh My Music Player — a terminal music player built with ratatui
/* ommp landing page.
 *
 * Every colour here is lifted from the application's own source rather than
 * invented, so the page and the thing it is advertising cannot drift apart.
 * The comments name where each one lives. */

/* Relative URLs, not root-absolute: this file is served from a repository
   subpath and the stylesheet's own name is content-hashed, but ../fonts/
   resolves correctly either way. */
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Bricolage Grotesque";
  src: url("../fonts/bricolage-grotesque-var.woff2") format("woff2");
  font-weight: 400 800; font-style: normal; font-display: swap;
}

:root {
  /* about_modal.rs */
  --bg:      #0A051E;   /* BG_DARK */
  --pink:    #FF3296;   /* NEON_PINK, the logo gradient's start */
  --cyan:    #00FFFF;   /* NEON_CYAN, its end */
  /* theme.rs */
  --sky:     #64DCFF;   /* border_focused, and the current track */
  --dim:     #46505F;   /* border_unfocused */
  --violet:  #C850FF;   /* progress_filled */
  /* status_bar.rs */
  --playing: #50FF78;

  --ink:     #EDEAF6;
  --ink-2:   #A9A6BC;
  --panel:   #120B2E;
  --line:    #241945;

  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --disp: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;

  /* Two measures, not one. Prose wants a short line; a section built around a
     screenshot wants room, and holding it to the prose width leaves the copy
     pinned against one edge with dead margin beyond it. */
  --wrap:      68rem;
  --wrap-wide: 78rem;
  --sb-h: 3.25rem;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

body {
  margin: 0;
  padding-bottom: var(--sb-h);
  background: var(--bg);
  color: var(--ink);
  font-family: var(--disp);
  font-size: clamp(1rem, .95rem + .2vw, 1.125rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* The terminal face is reserved for things that are literally the app:
   commands you type, keys you press, tab names, the status bar. */
code, kbd, .cmd, .kicker, .install-label, .tabstrip, .setup,
.sec-title-mono, .sb-row { font-family: var(--mono); }

a { color: var(--sky); text-underline-offset: .2em; text-decoration-thickness: 1px; }
a:hover { color: var(--cyan); }
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 2px; }

.skip {
  position: absolute; left: -9999px;
  background: var(--cyan); color: var(--bg); padding: .6rem 1rem; font-weight: 700;
}
.skip:focus { left: 1rem; top: 1rem; z-index: 100; }

/* Three limits, and the smallest wins. The 92% is what keeps the two measures
   moving together: without it, a viewport between the two caps (1128–1288px)
   pins the wide sections 20px off the edge while the prose sections stay 96px
   in, and the 76px stagger reads as a mistake. The fixed 2.5rem only takes over
   below ~500px, where a proportional gutter would be too tight to touch. */
.wrap      { width: min(100% - 2.5rem, 92%, var(--wrap));      margin-inline: auto; }
.wrap-wide { width: min(100% - 2.5rem, 92%, var(--wrap-wide)); margin-inline: auto; }

/* ── Hero ─────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  padding-block: clamp(3rem, 8vw, 6rem) 0;
  background-color: var(--bg);
}

/* The splash screen, as the page opens on. ommp scatters music notes across a
   dark field while it loads (about_modal.rs, render_splash_screen); the tile is
   that same scatter, generated from the same hash by tools/note-field.py. */
.hero::before {
  content: "";
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(60rem 32rem at 18% -8%, #3C145066, transparent 70%),
    radial-gradient(52rem 30rem at 88% 4%,  #143C5066, transparent 70%),
    url("../img/note-field.png") repeat;
  background-size: auto, auto, 370px 740px;
  -webkit-mask-image: linear-gradient(#000 60%, #0000 100%);
          mask-image: linear-gradient(#000 60%, #0000 100%);
}
.hero-in { position: relative; z-index: 1; }

.hero-in { text-align: center; }

.hero-logo {
  width: min(100%, 26rem); height: auto; display: block; margin: 0 auto 2.5rem;
  animation: rise .6s ease-out both;
}
@keyframes rise { from { opacity: 0; transform: translateY(.5rem); } to { opacity: 1; transform: none; } }

.kicker {
  margin: 0 0 1rem;
  font-size: .8125rem; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-2);
}

.display {
  font-family: var(--disp);
  font-weight: 700;
  font-size: clamp(2.4rem, 1.4rem + 4.4vw, 4.6rem);
  line-height: 1.02;
  letter-spacing: -.03em;
  margin: 0 0 1.5rem;
}
.display em {
  font-style: normal;
  background: linear-gradient(100deg, var(--pink), var(--cyan));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

.lede {
  max-width: 46rem; margin: 0 auto 2.5rem;
  color: var(--ink-2); font-size: 1.0625rem;
}

.installs { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; justify-content: center; }
.installs-left { justify-content: flex-start; margin-bottom: 2.5rem; }
.install { text-align: left; }
.install-label {
  display: block; font-size: .75rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-2); margin-bottom: .4rem;
}

/* One box, holding the command and the button that copies it. The border and
   the background belong to the box, not to either thing inside it, and the
   feedback is the icon changing, so nothing resizes when you copy. */
.cmdline {
  display: flex; align-items: center; gap: .5rem;
  width: max-content; max-width: 100%;
  background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
  padding: .35rem .4rem .35rem .9rem;
  transition: border-color .15s;
}
.cmdline:focus-within { border-color: var(--sky); }
.cmd {
  /* min-width:auto would refuse to shrink below the command's full width and
     push the box past a narrow viewport. */
  flex: 1 1 auto; min-width: 0; overflow-x: auto;
  font-family: var(--mono); font-size: .9375rem;
  color: var(--ink); white-space: nowrap;
  scrollbar-width: thin; scrollbar-color: var(--line) transparent;
}
/* The prompt lives in a pseudo-element, which never reaches the clipboard. */
.cmd::before { content: "$ "; color: var(--dim); }

.cmd-copy {
  flex: none; display: grid; place-items: center;
  width: 1.9rem; height: 1.9rem; padding: 0;
  background: none; border: 0; border-radius: 3px;
  color: var(--dim); cursor: pointer;
  transition: color .15s, background .15s;
}
.cmd-copy:hover { color: var(--ink); background: #ffffff12; }
.cmd-copy svg { width: 15px; height: 15px; }
.cmd-copy svg:last-child { display: none; }
.cmd-copy.is-copied { color: var(--playing); }
.cmd-copy.is-copied svg:first-child { display: none; }
.cmd-copy.is-copied svg:last-child { display: block; }

.hero-meta { margin-top: 2rem; color: var(--ink-2); font-size: .875rem; }

/* ── Screenshots ──────────────────────────────────────────────────── */

.shot { margin: 0; }
.shot img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: 6px;
}
.shot-hero {
  /* The one element allowed past the wide measure: it is the product, and it is
     the first thing you scroll into. */
  width: min(100% - 2.5rem, 92%, 84rem);
  margin: clamp(2.5rem, 5vw, 3.5rem) auto clamp(2.5rem, 5vw, 3.5rem);
}
.shot-hero img { box-shadow: 0 2rem 5rem -1.5rem #00000099; }
.shot-wide { margin-top: clamp(2.5rem, 5vw, 3.5rem); }

/* ── Bands ────────────────────────────────────────────────────────── */

.band { padding-block: clamp(4rem, 9vw, 7rem); }
.band-alt { background: #0D0725; border-block: 1px solid var(--line); }
.band-close { padding-bottom: clamp(3rem, 6vw, 4.5rem); }

.sec-title {
  font-family: var(--disp); font-weight: 600;
  font-size: clamp(1.7rem, 1.2rem + 1.9vw, 2.6rem);
  line-height: 1.1; letter-spacing: -.02em; margin: 0 0 1rem;
}
.sec-lede { max-width: 44rem; color: var(--ink-2); margin: 0 0 2.5rem; }
.sec-body { max-width: 44rem; color: var(--ink-2); margin: 0 0 1.1rem; font-size: .9375rem; }
.sec-body:last-child { margin-bottom: 0; }

/* The section that opens the page carries the weight the hero used to. */
.band-art { padding-top: clamp(2.5rem, 6vw, 4.5rem); }
.sec-title-lg { font-size: clamp(2rem, 1.3rem + 2.6vw, 3.2rem); }
.sec-lede-lg  { font-size: 1.0625rem; margin-bottom: 0; }
/* A mono heading, because the heading is keys. */
.sec-title-mono { font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.2rem); letter-spacing: 0; }

/* Two columns of prose under a full-width image, so a long explanation does not
   become one very long line. */
.two-up {
  display: grid; gap: clamp(1.5rem, 4vw, 3rem);
  color: var(--ink-2); font-size: .9375rem;
}
.two-up p { margin: 0; }
@media (min-width: 52rem) { .two-up { grid-template-columns: 1fr 1fr; } }

/* ── Split sections ───────────────────────────────────────────────── */

/* Copy in a fixed column, screenshot in the rest. The hero shot is the one that
   earns full width; from here down the screenshots illustrate a point the copy
   is already making, and blowing them up to fill the page only pushes the
   reader further from it. */
.split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: center; }
@media (min-width: 56rem) {
  .split { grid-template-columns: minmax(0, 26rem) 1fr; }
  /* Reordering alone leaves the figure in the narrow track and shrinks it to
     half the size of the one above. Mirror the tracks so both screenshots come
     out the same size, and only the side they sit on changes. */
  .split-reverse { grid-template-columns: 1fr minmax(0, 26rem); }
  .split-reverse .split-text { order: 2; }
}

/* ── The setup you skip ───────────────────────────────────────────── */

/* Four things you do not do, struck out, and the one line you do type. The
   argument is made by the ratio, so nothing here is decorated further. */
.setup { font-size: .9375rem; }
.setup-skipped { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: .7rem; }
.setup-skipped li { color: var(--dim); }
/* Drawn on the span rather than the list item: an inline box is the height of
   the text itself, so 50% of it lands on the middle of the letters instead of
   the middle of the line box, which sits higher. */
.setup-skipped span { position: relative; }
.setup-skipped span::after {
  content: ""; position: absolute; inset-inline: 0; top: 50%;
  height: 1px; background: var(--pink);
  transform-origin: left;
}

/* Once the script is running the lines arrive unstruck and are crossed out one
   at a time as the section comes into view. Without it they are simply already
   crossed out, which is the state that makes the point. */
.setup-armed .setup-skipped li { color: var(--ink-2); transition: color .4s ease; }
.setup-armed .setup-skipped span::after { transform: scaleX(0); transition: transform .45s ease; }
.setup-armed .setup-skipped li.is-struck { color: var(--dim); }
.setup-armed .setup-skipped li.is-struck span::after { transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .setup-armed .setup-skipped span::after { transition: none; }
}
.setup-real {
  margin: 0; font-weight: 700; font-size: 1.5rem;
  background: linear-gradient(100deg, var(--pink), var(--cyan));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.setup-real::before { content: "$ "; -webkit-text-fill-color: var(--dim); }

/* ── The tab strip, set as type ────────────────────────────────────── */

/* ommp's own tab bar. Same names, same order, same seven accents it draws them
   in — the separators included, because that is how the app renders them. */
.tabstrip {
  list-style: none; margin: 0 0 2.5rem; padding: 0;
  display: flex; flex-wrap: wrap; align-items: baseline;
  font-size: clamp(.875rem, .75rem + .4vw, 1.0625rem); font-weight: 700;
}
.tabstrip li { color: var(--accent); }
.tabstrip li + li::before {
  content: ""; color: var(--line); font-weight: 400;
  margin-inline: .75em; -webkit-text-fill-color: var(--line);
}

/* ── The small decisions ──────────────────────────────────────────── */

.details {
  list-style: none; margin: 0 0 2.5rem; padding: 0;
  display: grid; gap: 1.75rem;
}
@media (min-width: 46rem) { .details { grid-template-columns: 1fr 1fr; gap: 2rem 3rem; } }
.details li {
  border-top: 2px solid var(--accent); padding-top: 1rem;
  color: var(--ink-2); font-size: .9375rem;
}
.details li b { color: var(--ink); font-weight: 700; }
.details li i { font-style: italic; color: var(--ink); }
.details-close { max-width: 44rem; margin: 0; color: var(--ink-2); font-size: .9375rem; }

kbd {
  font-family: var(--mono); font-size: .8125rem; font-weight: 700;
  background: var(--panel); border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: 4px; padding: .2rem .5rem; color: var(--sky);
  display: inline-block; text-align: center;
}

/* ── Install ──────────────────────────────────────────────────────── */

.install-close { margin: 0; font-size: 1.0625rem; }

/* ── Footer ───────────────────────────────────────────────────────── */

.foot { border-top: 1px solid var(--line); padding-block: 2.5rem; }
.foot-in { display: grid; gap: 1rem; }
.foot-name {
  margin: 0; font-weight: 700; letter-spacing: .1em;
  background: linear-gradient(100deg, var(--pink), var(--cyan));
  -webkit-background-clip: text; background-clip: text; color: transparent;
  width: max-content;
}
.foot-links { display: flex; flex-wrap: wrap; gap: 1.25rem; font-size: .875rem; }
.foot-fine { margin: 0; color: var(--ink-2); font-size: .8125rem; }

/* ── The signature: ommp's status bar, tracking the page ──────────── */

.statusbar {
  position: fixed; inset: auto 0 0 0; z-index: 50;
  height: var(--sb-h);
  background: #0A051EF2;
  backdrop-filter: blur(8px);
  border-top: 1px solid var(--line);
  font-size: .8125rem;
}
.sb-fill {
  height: 2px; width: 0%;
  background: var(--violet);
  transition: width .1s linear;
}
.sb-row {
  height: calc(var(--sb-h) - 2px);
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 1rem;
  width: min(100% - 2.5rem, var(--wrap)); margin-inline: auto;
}
.sb-state { color: var(--playing); font-weight: 700; white-space: nowrap; }
.sb-track {
  color: var(--sky); text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sb-time { color: var(--ink-2); text-align: right; white-space: nowrap; }

@media (max-width: 34rem) {
  .sb-row { grid-template-columns: auto 1fr; }
  .sb-state { display: none; }
  .sb-time { font-size: .75rem; }
}