/* 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 can leave the layout viewport shorter than the screen - after the keyboard has been up, `100dvh` in a home-screen app comes back one status bar short - and the page behind the stage showed as a white strip along the bottom. Pinning the stage to the viewport's edges does not depend on that height at all, and the sea colour underneath covers whatever might still peek through. */ html, body { background: var(--stage-deep, #001b21); } .stage[data-phone] { position: fixed; inset: 0; height: auto; } .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 -- */ .m-title-row { display: flex; align-items: center; gap: 10px; padding: calc(14px + env(safe-area-inset-top)) var(--m-gutter) 4px; } .m-title-mascot { width: 44px; height: 44px; object-fit: contain; filter: drop-shadow(0 3px 8px oklch(15% 0.05 var(--hue) / 0.5)); } .m-title { flex: 1; font-size: 28px; font-weight: 900; color: var(--paper); letter-spacing: -0.01em; text-shadow: 0 2px 10px oklch(15% 0.05 var(--hue) / 0.45); } .m-status { 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; } /* Under the title row, overlap the row by the status bar's height: the bar keeps its place in the layout but already covers the status bar once it sticks. */ .m-title-row + .m-sticky { margin-top: calc(-1 * env(safe-area-inset-top)); } .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; }