/* The phone layout (components/mobile/). Every class is `m-` prefixed and only rendered when `usePhoneLayout` says so, so none of this reaches the kiosk or a tablet. */ :root { /* Liquid-glass surfaces over the sea gradient: a dark translucent fill, a strong blur with a saturation boost so colour behind it still glows through, and a thin bright rim along the top edge. */ --m-glass-fill: oklch(32% 0.04 var(--hue) / 0.42); --m-glass-rim: oklch(100% 0 0 / 0.22); --m-glass-shadow: 0 10px 30px oklch(10% 0.04 var(--hue) / 0.45); --m-gutter: 16px; /* Height of the floating dock, so scrolling content can clear it. */ --m-dock-clearance: calc(96px + env(safe-area-inset-bottom)); } /* iOS 26 can leave the layout viewport of a home-screen app one status bar short - `100dvh`, `innerHeight` and `position: fixed; inset: 0` all come back short, and WebKit draws nothing below the document - so the dock floated that far above the bottom over a dark strip. Sizing the stage alone was not enough: it got clipped at the short viewport. The document itself has to reach the bottom of the screen, so in standalone mode (`data-standalone` and `--full-height`, set by lib/fullHeight.ts) `` takes the larger of `100vh` and the screen's height, and the stage follows. Everywhere else the stage just pins to the viewport. The sea colour underneath covers whatever might still peek through. */ html, body { background: var(--stage-deep, #001b21); } :root[data-standalone] { height: max(100vh, var(--full-height, 0px)); overflow: hidden; } .stage[data-phone] { position: fixed; inset: 0; height: auto; } :root[data-standalone] .stage[data-phone] { bottom: auto; height: max(100vh, var(--full-height, 0px)); } .m-glass { background: var(--m-glass-fill); backdrop-filter: blur(14px) saturate(1.7); -webkit-backdrop-filter: blur(14px) saturate(1.7); border: 1px solid var(--m-glass-rim); box-shadow: var(--m-glass-shadow), inset 0 1px 0 oklch(100% 0 0 / 0.28), inset 0 -1px 0 oklch(100% 0 0 / 0.06); color: var(--paper); } .m-page { position: relative; z-index: 1; height: 100%; display: flex; flex-direction: column; } .m-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; /* No top inset here: the sticky bar reaches up under the status bar itself. */ padding: 0 0 calc(var(--m-dock-clearance) + 84px); animation: viewEnter 180ms ease-out; } /* ------------------------------------------------------------------ header -- */ /* Connection lost, or the figure on the reader - centred above the group switch, and only there while it has something to say. No app title above it: that only cost a row of space on a phone. */ .m-status { width: fit-content; margin: 0 auto 8px; font-size: 13px; font-weight: 800; color: var(--paper); background: oklch(97% 0.01 var(--hue) / 0.16); padding: 5px 12px; border-radius: 999px; white-space: nowrap; } .m-status[data-offline="true"] { color: oklch(80% 0.16 30); } /* The bar that stays put while the page scrolls under it. Sticks to the very top of the screen, under the status bar, so content never shows behind the clock. Frosted rather than painted: the background behind it is the ambience canvas, which takes its colour from whatever is playing, so any fixed fill was the wrong colour as soon as an album with different art started. A blur with a neutral darkening suits every one of them, and the mask fades it out instead of ending in a hard edge. */ .m-sticky { position: sticky; top: 0; z-index: 2; padding: calc(10px + env(safe-area-inset-top)) var(--m-gutter) 14px; } .m-sticky::before { content: ""; position: absolute; inset: 0; z-index: -1; background: oklch(0% 0 0 / 0.16); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); -webkit-mask-image: linear-gradient(180deg, #000 70%, transparent); mask-image: linear-gradient(180deg, #000 70%, transparent); pointer-events: none; } .m-navbar { display: flex; align-items: center; gap: 10px; padding-top: calc(12px + env(safe-area-inset-top)); } .m-navbar-title { flex: 1; min-width: 0; text-align: center; font-size: 19px; font-weight: 900; color: var(--paper); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .m-subtitle { text-align: center; margin: -6px var(--m-gutter) 14px; font-size: 13px; font-weight: 700; color: oklch(90% 0.02 var(--hue) / 0.75); } /* ------------------------------------------------------------- segmented -- */ .m-segmented { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: oklch(20% 0.04 var(--hue) / 0.38); backdrop-filter: blur(12px) saturate(1.6); -webkit-backdrop-filter: blur(12px) saturate(1.6); border: 1px solid oklch(100% 0 0 / 0.14); box-shadow: inset 0 1px 2px oklch(10% 0.04 var(--hue) / 0.35); } .m-segment { flex: 1; min-width: 0; border: none; background: transparent; color: oklch(95% 0.01 var(--hue) / 0.85); font: inherit; font-size: 14px; font-weight: 800; padding: 9px 6px; border-radius: 999px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: background 0.18s ease, color 0.18s ease; } .m-segment[data-active="true"] { background: var(--paper); color: var(--ink); box-shadow: 0 3px 10px oklch(10% 0.04 var(--hue) / 0.35); } /* ------------------------------------------------------------------ grids -- */ .m-category-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; padding: 0 var(--m-gutter); align-items: start; } .m-category-grid > .card { content-visibility: auto; contain-intrinsic-size: auto 230px; } .m-album-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 18px 12px; padding: 0 var(--m-gutter); align-items: start; } .m-album { display: flex; flex-direction: column; gap: 6px; min-width: 0; border: none; padding: 0; background: none; font: inherit; text-align: left; color: var(--paper); cursor: pointer; content-visibility: auto; contain-intrinsic-size: auto 170px; -webkit-tap-highlight-color: transparent; } .m-album:active .m-album-cover { transform: scale(0.96); } .m-album-cover { box-shadow: 0 6px 16px oklch(10% 0.04 var(--hue) / 0.45); transition: transform 0.12s ease; } .m-album[data-current="true"] .m-album-cover { outline: 3px solid var(--accent); outline-offset: 2px; } .m-album-title { font-size: 13px; font-weight: 800; line-height: 1.2; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .m-album-sub { margin-top: -3px; font-size: 11px; font-weight: 800; color: var(--accent-dim); } /* ------------------------------------------------------------------- songs -- */ .m-list { display: flex; flex-direction: column; gap: 2px; padding: 0 var(--m-gutter); } .m-song { display: flex; align-items: center; gap: 12px; padding: 8px 10px; border: none; border-radius: 14px; background: transparent; color: var(--paper); font: inherit; text-align: left; cursor: pointer; } .m-song:active, .m-song[data-current="true"] { background: oklch(97% 0.01 var(--hue) / 0.14); } .m-song-cover { height: 44px; flex: none; display: flex; } .m-song-text { flex: 1; min-width: 0; } .m-song-title, .m-song-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .m-song-title { font-size: 15px; font-weight: 800; } .m-song-sub { font-size: 12px; font-weight: 700; color: oklch(88% 0.02 var(--hue) / 0.65); } .m-song-time { font: 700 12px ui-monospace, Menlo, monospace; color: oklch(88% 0.02 var(--hue) / 0.6); } .m-hint { text-align: center; margin-top: 48px; padding: 0 var(--m-gutter); font-size: 18px; font-weight: 800; color: oklch(92% 0.02 var(--hue) / 0.8); } .m-hint img { width: 96px; height: 96px; object-fit: contain; opacity: 0.9; display: block; margin: 0 auto 8px; } /* -------------------------------------------------------------------- dock -- */ .m-dock { position: absolute; left: 0; right: 0; z-index: 3; display: flex; flex-direction: column; gap: 10px; padding: 0 12px calc(10px + env(safe-area-inset-bottom)); pointer-events: none; transition: bottom 0.2s ease; } /* With the keyboard up the home indicator is hidden behind it, so no safe-area gap. */ .m-dock[data-keyboard="true"] { padding-bottom: 8px; } .m-dock > * { pointer-events: auto; } .m-dock-row { display: flex; align-items: center; gap: 10px; /* Lets the row itself ignore taps between its pills. */ pointer-events: none; } .m-dock-row > * { pointer-events: auto; } .m-circle { flex: none; width: 58px; height: 58px; border-radius: 999px; display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; font: inherit; } .m-circle-small { flex: none; width: 40px; height: 40px; } .m-tabs { flex: 1; display: flex; padding: 4px; border-radius: 999px; height: 58px; } .m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px; border: none; border-radius: 999px; background: transparent; color: oklch(95% 0.01 var(--hue) / 0.8); font: inherit; cursor: pointer; transition: background 0.2s ease, color 0.2s ease; } .m-tab[data-active="true"] { background: oklch(97% 0.01 var(--hue) / 0.2); color: var(--paper); box-shadow: inset 0 1px 0 oklch(100% 0 0 / 0.2); } .m-tab-icon { font-size: 19px; line-height: 1; } .m-tab[data-active="true"] .m-tab-icon { color: var(--accent-dim); } .m-tab-label { font-size: 11px; font-weight: 800; } .m-search { flex: 1; min-width: 0; height: 58px; border-radius: 999px; display: flex; align-items: center; gap: 10px; padding: 0 10px 0 18px; margin: 0; font: inherit; } .m-search > svg { flex: none; opacity: 0.8; } .m-search input { flex: 1; min-width: 0; height: 100%; border: none; outline: none; background: transparent; color: var(--paper); font: inherit; /* 16px or more, or iOS zooms the page into the field on focus. */ font-size: 17px; font-weight: 700; -webkit-appearance: none; appearance: none; } .m-search input::placeholder { color: oklch(95% 0.01 var(--hue) / 0.55); } .m-search input::-webkit-search-cancel-button { display: none; } .m-search-closed { cursor: pointer; font-size: 17px; font-weight: 700; color: oklch(95% 0.01 var(--hue) / 0.7); } .m-search-clear { flex: none; width: 26px; height: 26px; border: none; border-radius: 999px; display: flex; align-items: center; justify-content: center; background: oklch(97% 0.01 var(--hue) / 0.3); color: var(--sea-deep); cursor: pointer; padding: 0; } /* ------------------------------------------------------------- mini player -- */ .m-mini { position: relative; display: flex; align-items: center; gap: 4px; height: 60px; padding: 0 8px 0 8px; border-radius: 20px; overflow: hidden; } .m-mini-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; height: 100%; border: none; background: none; padding: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; } .m-mini-cover { height: 44px; flex: none; display: flex; box-shadow: 0 3px 10px oklch(10% 0.04 var(--hue) / 0.4); border-radius: 8px; } .m-mini-text { min-width: 0; } .m-mini-title, .m-mini-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .m-mini-title { font-size: 15px; font-weight: 800; } .m-mini-sub { font-size: 12px; font-weight: 700; color: oklch(90% 0.02 var(--hue) / 0.7); } .m-mini-button { flex: none; width: 44px; height: 44px; border: none; border-radius: 999px; background: transparent; color: var(--paper); display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; } .m-mini-button:active { background: oklch(97% 0.01 var(--hue) / 0.15); } .m-mini-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--accent); transform-origin: left; transition: transform 0.5s linear; }