ommp.git

ommp

~ Oh My Music Player — a standalone terminal music player built with Rust and ratatui
git clone https://github.com/debuman/ommp.git

site/static/css/main.css (16065B)

      1 /* ommp landing page.
      2  *
      3  * Every colour here is lifted from the application's own source rather than
      4  * invented, so the page and the thing it is advertising cannot drift apart.
      5  * The comments name where each one lives. */
      6 
      7 /* Relative URLs, not root-absolute: this file is served from a repository
      8    subpath and the stylesheet's own name is content-hashed, but ../fonts/
      9    resolves correctly either way. */
     10 @font-face {
     11   font-family: "JetBrains Mono";
     12   src: url("../fonts/jetbrains-mono-400.woff2") format("woff2");
     13   font-weight: 400; font-style: normal; font-display: swap;
     14 }
     15 @font-face {
     16   font-family: "JetBrains Mono";
     17   src: url("../fonts/jetbrains-mono-700.woff2") format("woff2");
     18   font-weight: 700; font-style: normal; font-display: swap;
     19 }
     20 @font-face {
     21   font-family: "Bricolage Grotesque";
     22   src: url("../fonts/bricolage-grotesque-var.woff2") format("woff2");
     23   font-weight: 400 800; font-style: normal; font-display: swap;
     24 }
     25 
     26 :root {
     27   /* about_modal.rs */
     28   --bg:      #0A051E;   /* BG_DARK */
     29   --pink:    #FF3296;   /* NEON_PINK, the logo gradient's start */
     30   --cyan:    #00FFFF;   /* NEON_CYAN, its end */
     31   /* theme.rs */
     32   --sky:     #64DCFF;   /* border_focused, and the current track */
     33   --dim:     #46505F;   /* border_unfocused */
     34   --violet:  #C850FF;   /* progress_filled */
     35   /* status_bar.rs */
     36   --playing: #50FF78;
     37 
     38   --ink:     #EDEAF6;
     39   --ink-2:   #A9A6BC;
     40   --panel:   #120B2E;
     41   --line:    #241945;
     42 
     43   --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
     44   --disp: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
     45 
     46   /* Two measures, not one. Prose wants a short line; a section built around a
     47      screenshot wants room, and holding it to the prose width leaves the copy
     48      pinned against one edge with dead margin beyond it. */
     49   --wrap:      68rem;
     50   --wrap-wide: 78rem;
     51   --sb-h: 3.25rem;
     52 }
     53 
     54 *, *::before, *::after { box-sizing: border-box; }
     55 
     56 html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
     57 @media (prefers-reduced-motion: reduce) {
     58   html { scroll-behavior: auto; }
     59   *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
     60 }
     61 
     62 body {
     63   margin: 0;
     64   padding-bottom: var(--sb-h);
     65   background: var(--bg);
     66   color: var(--ink);
     67   font-family: var(--disp);
     68   font-size: clamp(1rem, .95rem + .2vw, 1.125rem);
     69   line-height: 1.6;
     70   -webkit-font-smoothing: antialiased;
     71 }
     72 
     73 /* The terminal face is reserved for things that are literally the app:
     74    commands you type, keys you press, tab names, the status bar. */
     75 code, kbd, .cmd, .kicker, .install-label, .tabstrip, .setup,
     76 .sec-title-mono, .sb-row { font-family: var(--mono); }
     77 
     78 a { color: var(--sky); text-underline-offset: .2em; text-decoration-thickness: 1px; }
     79 a:hover { color: var(--cyan); }
     80 :focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 2px; }
     81 
     82 .skip {
     83   position: absolute; left: -9999px;
     84   background: var(--cyan); color: var(--bg); padding: .6rem 1rem; font-weight: 700;
     85 }
     86 .skip:focus { left: 1rem; top: 1rem; z-index: 100; }
     87 
     88 /* Three limits, and the smallest wins. The 92% is what keeps the two measures
     89    moving together: without it, a viewport between the two caps (1128–1288px)
     90    pins the wide sections 20px off the edge while the prose sections stay 96px
     91    in, and the 76px stagger reads as a mistake. The fixed 2.5rem only takes over
     92    below ~500px, where a proportional gutter would be too tight to touch. */
     93 .wrap      { width: min(100% - 2.5rem, 92%, var(--wrap));      margin-inline: auto; }
     94 .wrap-wide { width: min(100% - 2.5rem, 92%, var(--wrap-wide)); margin-inline: auto; }
     95 
     96 /* ── Hero ─────────────────────────────────────────────────────────── */
     97 
     98 .hero {
     99   position: relative;
    100   padding-block: clamp(3rem, 8vw, 6rem) 0;
    101   background-color: var(--bg);
    102 }
    103 
    104 /* The splash screen, as the page opens on. ommp scatters music notes across a
    105    dark field while it loads (about_modal.rs, render_splash_screen); the tile is
    106    that same scatter, generated from the same hash by tools/note-field.py. */
    107 .hero::before {
    108   content: "";
    109   position: absolute; inset: 0; z-index: 0;
    110   pointer-events: none;
    111   background:
    112     radial-gradient(60rem 32rem at 18% -8%, #3C145066, transparent 70%),
    113     radial-gradient(52rem 30rem at 88% 4%,  #143C5066, transparent 70%),
    114     url("../img/note-field.png") repeat;
    115   background-size: auto, auto, 370px 740px;
    116   -webkit-mask-image: linear-gradient(#000 60%, #0000 100%);
    117           mask-image: linear-gradient(#000 60%, #0000 100%);
    118 }
    119 .hero-in { position: relative; z-index: 1; }
    120 
    121 .hero-in { text-align: center; }
    122 
    123 .hero-logo {
    124   width: min(100%, 26rem); height: auto; display: block; margin: 0 auto 2.5rem;
    125   animation: rise .6s ease-out both;
    126 }
    127 @keyframes rise { from { opacity: 0; transform: translateY(.5rem); } to { opacity: 1; transform: none; } }
    128 
    129 .kicker {
    130   margin: 0 0 1rem;
    131   font-size: .8125rem; letter-spacing: .22em; text-transform: uppercase;
    132   color: var(--ink-2);
    133 }
    134 
    135 .display {
    136   font-family: var(--disp);
    137   font-weight: 700;
    138   font-size: clamp(2.4rem, 1.4rem + 4.4vw, 4.6rem);
    139   line-height: 1.02;
    140   letter-spacing: -.03em;
    141   margin: 0 0 1.5rem;
    142 }
    143 .display em {
    144   font-style: normal;
    145   background: linear-gradient(100deg, var(--pink), var(--cyan));
    146   -webkit-background-clip: text; background-clip: text; color: transparent;
    147 }
    148 
    149 .lede {
    150   max-width: 46rem; margin: 0 auto 2.5rem;
    151   color: var(--ink-2); font-size: 1.0625rem;
    152 }
    153 
    154 .installs { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; justify-content: center; }
    155 .installs-left { justify-content: flex-start; margin-bottom: 2.5rem; }
    156 .install { text-align: left; }
    157 .install-label {
    158   display: block; font-size: .75rem; letter-spacing: .14em; text-transform: uppercase;
    159   color: var(--ink-2); margin-bottom: .4rem;
    160 }
    161 
    162 /* One box, holding the command and the button that copies it. The border and
    163    the background belong to the box, not to either thing inside it, and the
    164    feedback is the icon changing, so nothing resizes when you copy. */
    165 .cmdline {
    166   display: flex; align-items: center; gap: .5rem;
    167   width: max-content; max-width: 100%;
    168   background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
    169   padding: .35rem .4rem .35rem .9rem;
    170   transition: border-color .15s;
    171 }
    172 .cmdline:focus-within { border-color: var(--sky); }
    173 .cmd {
    174   /* min-width:auto would refuse to shrink below the command's full width and
    175      push the box past a narrow viewport. */
    176   flex: 1 1 auto; min-width: 0; overflow-x: auto;
    177   font-family: var(--mono); font-size: .9375rem;
    178   color: var(--ink); white-space: nowrap;
    179   scrollbar-width: thin; scrollbar-color: var(--line) transparent;
    180 }
    181 /* The prompt lives in a pseudo-element, which never reaches the clipboard. */
    182 .cmd::before { content: "$ "; color: var(--dim); }
    183 
    184 .cmd-copy {
    185   flex: none; display: grid; place-items: center;
    186   width: 1.9rem; height: 1.9rem; padding: 0;
    187   background: none; border: 0; border-radius: 3px;
    188   color: var(--dim); cursor: pointer;
    189   transition: color .15s, background .15s;
    190 }
    191 .cmd-copy:hover { color: var(--ink); background: #ffffff12; }
    192 .cmd-copy svg { width: 15px; height: 15px; }
    193 .cmd-copy svg:last-child { display: none; }
    194 .cmd-copy.is-copied { color: var(--playing); }
    195 .cmd-copy.is-copied svg:first-child { display: none; }
    196 .cmd-copy.is-copied svg:last-child { display: block; }
    197 
    198 .hero-meta { margin-top: 2rem; color: var(--ink-2); font-size: .875rem; }
    199 
    200 /* ── Screenshots ──────────────────────────────────────────────────── */
    201 
    202 .shot { margin: 0; }
    203 .shot img {
    204   display: block; width: 100%; height: auto;
    205   border: 1px solid var(--line); border-radius: 6px;
    206 }
    207 .shot-hero {
    208   /* The one element allowed past the wide measure: it is the product, and it is
    209      the first thing you scroll into. */
    210   width: min(100% - 2.5rem, 92%, 84rem);
    211   margin: clamp(2.5rem, 5vw, 3.5rem) auto clamp(2.5rem, 5vw, 3.5rem);
    212 }
    213 .shot-hero img { box-shadow: 0 2rem 5rem -1.5rem #00000099; }
    214 .shot-wide { margin-top: clamp(2.5rem, 5vw, 3.5rem); }
    215 
    216 /* ── Bands ────────────────────────────────────────────────────────── */
    217 
    218 .band { padding-block: clamp(4rem, 9vw, 7rem); }
    219 .band-alt { background: #0D0725; border-block: 1px solid var(--line); }
    220 .band-close { padding-bottom: clamp(3rem, 6vw, 4.5rem); }
    221 
    222 .sec-title {
    223   font-family: var(--disp); font-weight: 600;
    224   font-size: clamp(1.7rem, 1.2rem + 1.9vw, 2.6rem);
    225   line-height: 1.1; letter-spacing: -.02em; margin: 0 0 1rem;
    226 }
    227 .sec-lede { max-width: 44rem; color: var(--ink-2); margin: 0 0 2.5rem; }
    228 .sec-body { max-width: 44rem; color: var(--ink-2); margin: 0 0 1.1rem; font-size: .9375rem; }
    229 .sec-body:last-child { margin-bottom: 0; }
    230 
    231 /* The section that opens the page carries the weight the hero used to. */
    232 .band-art { padding-top: clamp(2.5rem, 6vw, 4.5rem); }
    233 .sec-title-lg { font-size: clamp(2rem, 1.3rem + 2.6vw, 3.2rem); }
    234 .sec-lede-lg  { font-size: 1.0625rem; margin-bottom: 0; }
    235 /* A mono heading, because the heading is keys. */
    236 .sec-title-mono { font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.2rem); letter-spacing: 0; }
    237 
    238 /* Two columns of prose under a full-width image, so a long explanation does not
    239    become one very long line. */
    240 .two-up {
    241   display: grid; gap: clamp(1.5rem, 4vw, 3rem);
    242   color: var(--ink-2); font-size: .9375rem;
    243 }
    244 .two-up p { margin: 0; }
    245 @media (min-width: 52rem) { .two-up { grid-template-columns: 1fr 1fr; } }
    246 
    247 /* ── Split sections ───────────────────────────────────────────────── */
    248 
    249 /* Copy in a fixed column, screenshot in the rest. The hero shot is the one that
    250    earns full width; from here down the screenshots illustrate a point the copy
    251    is already making, and blowing them up to fill the page only pushes the
    252    reader further from it. */
    253 .split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: center; }
    254 @media (min-width: 56rem) {
    255   .split { grid-template-columns: minmax(0, 26rem) 1fr; }
    256   /* Reordering alone leaves the figure in the narrow track and shrinks it to
    257      half the size of the one above. Mirror the tracks so both screenshots come
    258      out the same size, and only the side they sit on changes. */
    259   .split-reverse { grid-template-columns: 1fr minmax(0, 26rem); }
    260   .split-reverse .split-text { order: 2; }
    261 }
    262 
    263 /* ── The setup you skip ───────────────────────────────────────────── */
    264 
    265 /* Four things you do not do, struck out, and the one line you do type. The
    266    argument is made by the ratio, so nothing here is decorated further. */
    267 .setup { font-size: .9375rem; }
    268 .setup-skipped { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: .7rem; }
    269 .setup-skipped li { color: var(--dim); }
    270 /* Drawn on the span rather than the list item: an inline box is the height of
    271    the text itself, so 50% of it lands on the middle of the letters instead of
    272    the middle of the line box, which sits higher. */
    273 .setup-skipped span { position: relative; }
    274 .setup-skipped span::after {
    275   content: ""; position: absolute; inset-inline: 0; top: 50%;
    276   height: 1px; background: var(--pink);
    277   transform-origin: left;
    278 }
    279 
    280 /* Once the script is running the lines arrive unstruck and are crossed out one
    281    at a time as the section comes into view. Without it they are simply already
    282    crossed out, which is the state that makes the point. */
    283 .setup-armed .setup-skipped li { color: var(--ink-2); transition: color .4s ease; }
    284 .setup-armed .setup-skipped span::after { transform: scaleX(0); transition: transform .45s ease; }
    285 .setup-armed .setup-skipped li.is-struck { color: var(--dim); }
    286 .setup-armed .setup-skipped li.is-struck span::after { transform: scaleX(1); }
    287 @media (prefers-reduced-motion: reduce) {
    288   .setup-armed .setup-skipped span::after { transition: none; }
    289 }
    290 .setup-real {
    291   margin: 0; font-weight: 700; font-size: 1.5rem;
    292   background: linear-gradient(100deg, var(--pink), var(--cyan));
    293   -webkit-background-clip: text; background-clip: text; color: transparent;
    294 }
    295 .setup-real::before { content: "$ "; -webkit-text-fill-color: var(--dim); }
    296 
    297 /* ── The tab strip, set as type ────────────────────────────────────── */
    298 
    299 /* ommp's own tab bar. Same names, same order, same seven accents it draws them
    300    in — the separators included, because that is how the app renders them. */
    301 .tabstrip {
    302   list-style: none; margin: 0 0 2.5rem; padding: 0;
    303   display: flex; flex-wrap: wrap; align-items: baseline;
    304   font-size: clamp(.875rem, .75rem + .4vw, 1.0625rem); font-weight: 700;
    305 }
    306 .tabstrip li { color: var(--accent); }
    307 .tabstrip li + li::before {
    308   content: "│"; color: var(--line); font-weight: 400;
    309   margin-inline: .75em; -webkit-text-fill-color: var(--line);
    310 }
    311 
    312 /* ── The small decisions ──────────────────────────────────────────── */
    313 
    314 .details {
    315   list-style: none; margin: 0 0 2.5rem; padding: 0;
    316   display: grid; gap: 1.75rem;
    317 }
    318 @media (min-width: 46rem) { .details { grid-template-columns: 1fr 1fr; gap: 2rem 3rem; } }
    319 .details li {
    320   border-top: 2px solid var(--accent); padding-top: 1rem;
    321   color: var(--ink-2); font-size: .9375rem;
    322 }
    323 .details li b { color: var(--ink); font-weight: 700; }
    324 .details li i { font-style: italic; color: var(--ink); }
    325 .details-close { max-width: 44rem; margin: 0; color: var(--ink-2); font-size: .9375rem; }
    326 
    327 kbd {
    328   font-family: var(--mono); font-size: .8125rem; font-weight: 700;
    329   background: var(--panel); border: 1px solid var(--line); border-bottom-width: 2px;
    330   border-radius: 4px; padding: .2rem .5rem; color: var(--sky);
    331   display: inline-block; text-align: center;
    332 }
    333 
    334 /* ── Install ──────────────────────────────────────────────────────── */
    335 
    336 .install-close { margin: 0; font-size: 1.0625rem; }
    337 
    338 /* ── Footer ───────────────────────────────────────────────────────── */
    339 
    340 .foot { border-top: 1px solid var(--line); padding-block: 2.5rem; }
    341 .foot-in { display: grid; gap: 1rem; }
    342 .foot-name {
    343   margin: 0; font-weight: 700; letter-spacing: .1em;
    344   background: linear-gradient(100deg, var(--pink), var(--cyan));
    345   -webkit-background-clip: text; background-clip: text; color: transparent;
    346   width: max-content;
    347 }
    348 .foot-links { display: flex; flex-wrap: wrap; gap: 1.25rem; font-size: .875rem; }
    349 .foot-fine { margin: 0; color: var(--ink-2); font-size: .8125rem; }
    350 
    351 /* ── The signature: ommp's status bar, tracking the page ──────────── */
    352 
    353 .statusbar {
    354   position: fixed; inset: auto 0 0 0; z-index: 50;
    355   height: var(--sb-h);
    356   background: #0A051EF2;
    357   backdrop-filter: blur(8px);
    358   border-top: 1px solid var(--line);
    359   font-size: .8125rem;
    360 }
    361 .sb-fill {
    362   height: 2px; width: 0%;
    363   background: var(--violet);
    364   transition: width .1s linear;
    365 }
    366 .sb-row {
    367   height: calc(var(--sb-h) - 2px);
    368   display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
    369   gap: 1rem;
    370   width: min(100% - 2.5rem, var(--wrap)); margin-inline: auto;
    371 }
    372 .sb-state { color: var(--playing); font-weight: 700; white-space: nowrap; }
    373 .sb-track {
    374   color: var(--sky); text-align: center;
    375   overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    376 }
    377 .sb-time { color: var(--ink-2); text-align: right; white-space: nowrap; }
    378 
    379 @media (max-width: 34rem) {
    380   .sb-row { grid-template-columns: auto 1fr; }
    381   .sb-state { display: none; }
    382   .sb-time { font-size: .75rem; }
    383 }