/* score-reader.css — Full-page score reader layout. Used only on /music/{slug}/score/ pages via score-reader-default.html. Two ideas drive the design. The sheet is the only bright object on screen — it sits on the theme's ground with a hairline edge and a soft shadow, reading as paper on a stand rather than as a document bleeding into the background. And the chrome recedes: after a few seconds of stillness the bar and the page-turn affordances fade, because a score reader should show a score. */ .score-reader-page { /* Bar height. The viewport is offset by exactly this much and does not reflow when the bar fades — recomputing the fit on every idle transition would make the page jump while being read. */ --score-chrome: 2.75rem; --score-gutter: 1.25rem; /* Paper. Set per theme rather than derived, because "slightly brighter than the ground" is a different mix in a warm off-white, a near-black, and a brown. */ --score-paper: #fffdfa; overflow: hidden; /* the viewport scrolls, not the document */ } [data-theme="dark"] .score-reader-page { --score-paper: #1c1c1c; } [data-theme="cappuccino"] .score-reader-page { --score-paper: #5e4230; } @media (prefers-color-scheme: dark) { :root:not([data-theme]) .score-reader-page { --score-paper: #1c1c1c; } } /* ------------------------------------------------------------------ Top bar ------------------------------------------------------------------ */ .score-reader-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; display: flex; align-items: center; gap: 1rem; padding: 0 1.25rem; height: var(--score-chrome); background: var(--bg); border-bottom: 1px solid var(--border); font-family: var(--font-sans); font-size: 0.825rem; transition: opacity 0.4s ease, transform 0.4s ease; } .score-reader-bar-left { flex: 0 0 auto; } .score-reader-movements { display: flex; align-items: center; gap: 0.25rem; flex: 1 1 auto; overflow-x: auto; scrollbar-width: none; } .score-reader-movements::-webkit-scrollbar { display: none; } .score-reader-bar-right { display: flex; align-items: center; gap: 0.75rem; flex: 0 0 auto; } .score-reader-back { color: var(--text-muted); text-decoration: none; white-space: nowrap; } .score-reader-back:hover { color: var(--text); } .score-reader-mvt { background: none; border: none; color: var(--text-muted); font-family: var(--font-sans); font-size: 0.8rem; padding: 0.2rem 0.5rem; cursor: pointer; white-space: nowrap; border-radius: 3px; transition: background 0.15s, color 0.15s; } .score-reader-mvt:hover, .score-reader-mvt.is-active { background: var(--bg-subtle); color: var(--text); } .score-reader-pdf { color: var(--text-muted); text-decoration: none; font-size: 0.8rem; } .score-reader-pdf:hover { color: var(--text); } /* Zoom group ------------------------------------------------------- */ .score-reader-zoom { display: flex; align-items: center; border: 1px solid var(--border); border-radius: 3px; overflow: hidden; } .score-reader-zoom-btn { background: none; border: none; color: var(--text-muted); font-family: var(--font-sans); font-size: 0.75rem; line-height: 1; padding: 0.35rem 0.5rem; cursor: pointer; transition: background 0.15s, color 0.15s; } .score-reader-zoom-btn + .score-reader-zoom-btn { border-left: 1px solid var(--border); } .score-reader-zoom-btn:hover:not(:disabled) { background: var(--bg-subtle); color: var(--text); } .score-reader-zoom-btn:disabled { opacity: 0.35; cursor: default; } .score-reader-zoom-mode { min-width: 3.6em; font-variant-caps: all-small-caps; letter-spacing: 0.06em; font-variant-numeric: tabular-nums; } /* ------------------------------------------------------------------ Stage and viewport ------------------------------------------------------------------ */ .score-reader-stage { position: fixed; inset: 0; background: var(--bg); } .score-reader-viewport { position: absolute; top: var(--score-chrome); left: 0; right: 0; bottom: 0; overflow: auto; display: flex; padding: var(--score-gutter); overscroll-behavior: contain; } /* `margin: auto` rather than `justify-content: center`: it centres the sheet while it fits, but unlike the flex property it does not clip the leading edge once the sheet is larger than the viewport — which is the whole point of zooming into an orchestral page. */ .score-page { margin: auto; flex: 0 0 auto; position: relative; /* Page 1's width/height, written into the element by the build. */ --ar: var(--score-aspect-num, 1.294); aspect-ratio: var(--ar) / 1; /* Fit-to-height without scripting. The width has to be derived from the viewport height rather than capped with max-height: `width` would be a definite size, so a max-height clamp shortens the box without narrowing it, and the aspect ratio breaks instead of the box shrinking. min() keeps a tall page from overflowing a narrow screen. */ --score-fit-width: min(100%, calc((100vh - var(--score-chrome) - 2 * var(--score-gutter)) * var(--ar))); width: var(--score-fit-width); } /* A portrait sheet fills the width instead and scrolls, matching what the reader's own default does once its script is running. */ .score-page[data-orient="portrait"] { --score-fit-width: 100%; background: var(--score-paper); border: 1px solid var(--border); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 28px rgba(0, 0, 0, 0.06); /* The ink colour every inlined score inherits. */ color: var(--text); /* `fill` is an inherited SVG property, and it crosses the HTML→SVG boundary — so any element that never declares a fill of its own (the common case for glyph outlines) draws in the theme's ink. `stroke` is deliberately NOT set here: its initial value is `none`, and inheriting a stroke would outline every filled notehead. */ fill: currentColor; } /* Once the reader is live, zoom owns the sheet's width and the fit caps have to come off — otherwise max-height would clamp every zoom level back to one screenful. */ .js-ready .score-page { /* Falls back to the same fit the stylesheet computes on its own, not to 100%. --score-page-width is only set once the first page has been fetched and measured, and on a 1.5 MB orchestral page that is a long gap to spend at full container width. */ width: var(--score-page-width, var(--score-fit-width)); } /* Recolour the engraver's own values instead of rewriting them at build time. A presentation attribute (fill="#000000") carries specificity zero, so any author-stylesheet rule beats it — no per-engraver build step, and nothing to change when a fourth engraver appears. Ink and ground are mapped separately, and everything else is left alone. Recolouring *every* fill is the obvious shortcut and it is wrong: MuseScore opens each page with a full-bleed white path, so a blanket rule paints an opaque sheet of ink over notation that was themed correctly underneath it. LilyPond emits no white at all, which is why that only shows up on a real MuseScore export. Leaving unmatched colours untouched also preserves intentional colour — analytical markings, cue-note grey, a coloured ossia — instead of flattening it into the text colour. */ .score-page svg [fill="#000000" i], .score-page svg [fill="#000" i], .score-page svg [fill="black" i] { fill: currentColor; } .score-page svg [stroke="#000000" i], .score-page svg [stroke="#000" i], .score-page svg [stroke="black" i] { stroke: currentColor; } /* Page grounds join the sheet rather than staying paper-white in a dark theme. `none` is untouched throughout — slurs, ties and beams need it. */ .score-page svg [fill="#ffffff" i], .score-page svg [fill="#fff" i], .score-page svg [fill="white" i] { fill: var(--score-paper); } .score-page svg { display: block; width: 100%; height: 100%; } /* Scripting-off fallback. Renders in the engraver's own colours, which is an honest degradation — legible in any theme, if not native to it. */ .score-page-fallback { display: block; width: 100%; height: auto; } .score-page.is-loading { opacity: 0.35; transition: opacity 0.2s ease 0.15s; } .score-page-error { display: flex; align-items: center; justify-content: center; height: 100%; margin: 0; padding: 1rem; text-align: center; font-family: var(--font-sans); font-size: 0.85rem; color: var(--text-muted); } /* ------------------------------------------------------------------ Page turning ------------------------------------------------------------------ */ .score-reader-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 50; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; background: var(--bg); border: 1px solid var(--border); border-radius: 50%; color: var(--text-muted); font-size: 1.15rem; cursor: pointer; transition: color 0.15s, border-color 0.15s, opacity 0.4s ease; } .score-reader-nav:hover:not(:disabled) { color: var(--text); border-color: var(--text-muted); } .score-reader-nav:disabled { opacity: 0.25; cursor: default; } .score-reader-prev { left: 0.85rem; } .score-reader-next { right: 0.85rem; } /* Folio — the page number as it would be printed on the sheet, rather than as a counter chip in the corner of an application. */ .score-folio { position: absolute; bottom: 0.7rem; left: 50%; transform: translateX(-50%); z-index: 50; font-family: var(--font-sans); font-size: 0.72rem; font-variant-caps: all-small-caps; font-variant-numeric: tabular-nums; letter-spacing: 0.07em; color: var(--text-faint); pointer-events: none; transition: opacity 0.4s ease; } /* ------------------------------------------------------------------ Instrument-label gutter A second copy of the page, clipped to its label column and translated to sit against the viewport's left edge, so the instrument names stay readable once the real ones have been panned off. It scrolls vertically with the page — it is positioned inside the sheet, not inside the viewport — which is what keeps each name beside its own staff. ------------------------------------------------------------------ */ .score-gutter { position: absolute; left: 0; top: 0; bottom: 0; z-index: 2; width: var(--score-gutter-w, 0px); overflow: hidden; background: var(--score-paper); border-right: 1px solid var(--border); box-shadow: 3px 0 8px rgba(0, 0, 0, 0.07); /* The page underneath owns every drag, swipe and click. */ pointer-events: none; opacity: 0; transition: opacity 0.15s ease; } .score-gutter.is-visible { opacity: 1; } /* Full page width inside the clip, so the copy lines up with the original rather than being squashed into the gutter. The width is set from the measured page box in updateGutter(); this is only the starting value. */ .score-gutter-inner { width: 100%; height: 100%; } .score-gutter-inner svg { display: block; width: 100%; height: 100%; } /* Zoomed past the viewport, the folio is over the staves rather than in the page's bottom margin, so it takes a ground of its own. */ .is-panning .score-folio { background: var(--bg); border: 1px solid var(--border); border-radius: 999px; padding: 0.1rem 0.55rem; } /* ------------------------------------------------------------------ Idle — chrome recedes ------------------------------------------------------------------ */ .score-reader-page.is-idle .score-reader-bar { opacity: 0; transform: translateY(-100%); pointer-events: none; } .score-reader-page.is-idle .score-reader-nav, .score-reader-page.is-idle .score-folio { opacity: 0; pointer-events: none; } /* ------------------------------------------------------------------ Narrow screens ------------------------------------------------------------------ */ @media (max-width: 640px) { .score-reader-page { --score-gutter: 0.5rem; } /* Swipe replaces them; at this width they would sit on the music. */ .score-reader-nav { display: none; } .score-reader-zoom { display: none; } } @media (prefers-reduced-motion: reduce) { .score-reader-bar, .score-reader-nav, .score-folio, .score-gutter, .score-page.is-loading { transition: none; } .score-reader-page.is-idle .score-reader-bar { transform: none; } } /* ------------------------------------------------------------------ Composition landing page additions (metadata block) ------------------------------------------------------------------ */ .composition-details { display: flex; flex-wrap: wrap; gap: 0 1.5rem; color: var(--text-muted); font-family: var(--font-sans); font-size: 0.875rem; } .comp-detail { white-space: nowrap; } .composition-actions { display: flex; gap: 0.75rem; align-items: center; } .comp-btn { display: inline-block; padding: 0.35em 0.9em; border: 1px solid var(--border); border-radius: 3px; font-family: var(--font-sans); font-size: 0.825rem; text-decoration: none; color: var(--text); background: var(--bg); transition: background 0.15s, border-color 0.15s; } .comp-btn:hover { background: var(--bg-subtle); border-color: var(--text-muted); } .comp-btn--secondary { color: var(--text-muted); } /* ------------------------------------------------------------------ Movement list ------------------------------------------------------------------ */ .composition-movements { border-top: 1px solid var(--border); margin: 1.5rem 0; padding-top: 1rem; display: flex; flex-direction: column; gap: 1rem; } .comp-movement-header { display: flex; align-items: baseline; gap: 0.75rem; font-family: var(--font-sans); font-size: 0.875rem; } .comp-movement-name { font-weight: 600; color: var(--text); } .comp-movement-duration { color: var(--text-muted); font-size: 0.8rem; } .comp-movement-score { color: var(--text-muted); font-size: 0.8rem; text-decoration: none; margin-left: auto; } .comp-movement-score:hover { color: var(--text); } .movement-audio { width: 100%; margin-top: 0.4rem; accent-color: var(--text-muted); }