The hands' drop-shadow outline sat on their own compositor layers, so the GPU recomputed it every frame a bubble moved. It is now on an inner box, painted once into the hand's layer. The bubbles rise beside the watch, never behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
560 lines
12 KiB
CSS
560 lines
12 KiB
CSS
/* The clock overlay (ClockOverlay.tsx), ported from `claude-design/Delphin Uhr.dc.html`.
|
|
Sized in vmin like the design, so the same watch fills a 1080p kiosk and a phone held
|
|
upright. */
|
|
|
|
.clock-overlay {
|
|
--clock-ink: oklch(28% 0.04 220);
|
|
--clock-pink: oklch(70% 0.16 340);
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 8;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6vmin;
|
|
background: linear-gradient(
|
|
180deg,
|
|
oklch(55% 0.07 var(--hue)) 0%,
|
|
oklch(38% 0.06 var(--hue)) 45%,
|
|
oklch(20% 0.045 var(--hue)) 100%
|
|
);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
will-change: opacity;
|
|
}
|
|
|
|
/* Hidden by opacity, not `display: none`: the watch stays styled, laid out and
|
|
rasterized, so Pos1 or the remote's Guide key shows it in the next frame instead of
|
|
after the Pi has painted every gradient of the dial from scratch. `display: flex`
|
|
above would otherwise beat the `hidden` attribute anyway. */
|
|
.clock-overlay[hidden] {
|
|
display: flex;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Upright screens: the countdowns go under the watch instead of beside it. */
|
|
@media (orientation: portrait) {
|
|
.clock-overlay {
|
|
flex-direction: column;
|
|
gap: 4vmin;
|
|
}
|
|
}
|
|
|
|
.clock-badge {
|
|
position: absolute;
|
|
top: calc(18px + var(--safe-top));
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 1.2vmin 3vmin;
|
|
border-radius: 999px;
|
|
font-size: max(16px, 3vmin);
|
|
font-weight: 900;
|
|
white-space: nowrap;
|
|
color: var(--clock-ink);
|
|
background: oklch(97% 0.01 var(--hue) / 0.75);
|
|
box-shadow: 0 1vmin 3vmin oklch(15% 0.05 var(--hue) / 0.3);
|
|
}
|
|
|
|
.clock-badge[data-school] {
|
|
background: oklch(92% 0.08 85 / 0.92);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- the watch */
|
|
|
|
.clock-watch {
|
|
position: relative;
|
|
flex: none;
|
|
width: 78vmin;
|
|
height: 96vmin;
|
|
}
|
|
|
|
.clock-strap {
|
|
position: absolute;
|
|
width: 15vmin;
|
|
height: 24vmin;
|
|
box-sizing: border-box;
|
|
border: 0.55vmin solid var(--clock-ink);
|
|
--strap: oklch(70% 0.16 340);
|
|
box-shadow: 0 3vmin 5vmin oklch(15% 0.05 210 / 0.35);
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--strap) 0%,
|
|
var(--strap) 70%,
|
|
oklch(from var(--strap) calc(l - 0.08) c h) 100%
|
|
);
|
|
}
|
|
|
|
.clock-strap[data-pos^="top"] {
|
|
top: 0;
|
|
border-radius: 2.4vmin 2.4vmin 0.8vmin 0.8vmin;
|
|
transform-origin: 50% 100%;
|
|
}
|
|
|
|
.clock-strap[data-pos^="bottom"] {
|
|
bottom: 0;
|
|
border-radius: 0.8vmin 0.8vmin 2.4vmin 2.4vmin;
|
|
transform-origin: 50% 0%;
|
|
}
|
|
|
|
.clock-strap[data-pos="top-left"] {
|
|
left: 16vmin;
|
|
transform: perspective(40vmin) rotateX(10deg) rotate(-8deg);
|
|
}
|
|
|
|
.clock-strap[data-pos="top-right"] {
|
|
right: 16vmin;
|
|
--strap: oklch(66% 0.12 205);
|
|
transform: perspective(40vmin) rotateX(10deg) rotate(8deg);
|
|
}
|
|
|
|
.clock-strap[data-pos="bottom-left"] {
|
|
left: 16vmin;
|
|
--strap: oklch(64% 0.11 175);
|
|
transform: perspective(40vmin) rotateX(-10deg) rotate(8deg);
|
|
}
|
|
|
|
.clock-strap[data-pos="bottom-right"] {
|
|
right: 16vmin;
|
|
--strap: oklch(58% 0.12 245);
|
|
transform: perspective(40vmin) rotateX(-10deg) rotate(-8deg);
|
|
}
|
|
|
|
.clock-button {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 6vmin;
|
|
height: 10vmin;
|
|
margin: -5vmin 0 0 -3vmin;
|
|
box-sizing: border-box;
|
|
border-radius: 1.6vmin;
|
|
border: 0.55vmin solid var(--clock-ink);
|
|
}
|
|
|
|
.clock-button[data-hue="pink"] {
|
|
background: var(--clock-pink);
|
|
}
|
|
|
|
.clock-button[data-hue="205"] {
|
|
background: oklch(66% 0.12 205);
|
|
}
|
|
|
|
.clock-face {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 9vmin;
|
|
width: 78vmin;
|
|
height: 78vmin;
|
|
box-sizing: border-box;
|
|
border-radius: 50%;
|
|
border: 0.6vmin solid var(--clock-ink);
|
|
/* The watch's shadow. It used to be a drop-shadow filter on the whole watch, which
|
|
made the Pi re-render and re-blur the entire dial on every tick of the second
|
|
hand. */
|
|
box-shadow: 0 3vmin 5vmin oklch(15% 0.05 210 / 0.5);
|
|
background:
|
|
radial-gradient(
|
|
circle closest-side,
|
|
transparent 88.6%,
|
|
var(--clock-ink) 88.6%,
|
|
var(--clock-ink) 89.6%,
|
|
transparent 89.6%
|
|
),
|
|
conic-gradient(
|
|
from 210deg,
|
|
oklch(94% 0.008 210),
|
|
oklch(81% 0.015 210),
|
|
oklch(96% 0.006 210),
|
|
oklch(79% 0.015 210),
|
|
oklch(94% 0.008 210)
|
|
);
|
|
}
|
|
|
|
.clock-face > * {
|
|
position: absolute;
|
|
}
|
|
|
|
/* The bezel's 48 dots: big coloured ones on the 5-minute marks, small grey between. */
|
|
.clock-dot {
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 1.1vmin;
|
|
height: 1.1vmin;
|
|
border-radius: 50%;
|
|
background: oklch(62% 0.02 220);
|
|
box-shadow: inset 0 0 0 0.2vmin oklch(30% 0.03 220 / 0.35);
|
|
}
|
|
|
|
.clock-dot[data-big] {
|
|
width: 1.9vmin;
|
|
height: 1.9vmin;
|
|
}
|
|
|
|
.clock-dot[data-hue="205"] {
|
|
background: oklch(64% 0.12 205);
|
|
}
|
|
|
|
.clock-dot[data-hue="175"] {
|
|
background: oklch(64% 0.12 175);
|
|
}
|
|
|
|
.clock-dot[data-hue="245"] {
|
|
background: oklch(64% 0.12 245);
|
|
}
|
|
|
|
.clock-dot[data-hue="pink"] {
|
|
background: var(--clock-pink);
|
|
}
|
|
|
|
/* Every ring below is centred on the face; only the diameter differs. */
|
|
.clock-face-inner,
|
|
.clock-sections,
|
|
.clock-sections-gaps,
|
|
.clock-sections-rim,
|
|
.clock-center,
|
|
.clock-minute-track,
|
|
.clock-minute-ticks,
|
|
.clock-minute-ticks-major {
|
|
left: 50%;
|
|
top: 50%;
|
|
width: var(--d);
|
|
height: var(--d);
|
|
margin: calc(var(--d) / -2) 0 0 calc(var(--d) / -2);
|
|
box-sizing: border-box;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.clock-face-inner {
|
|
--d: 68.4vmin;
|
|
background: oklch(99% 0.005 210);
|
|
box-shadow: inset 0 0.6vmin 1.4vmin oklch(30% 0.03 220 / 0.18);
|
|
}
|
|
|
|
.clock-sections {
|
|
--d: 55.6vmin;
|
|
background: conic-gradient(
|
|
from -15deg,
|
|
oklch(91% 0.06 205) 0deg 30deg,
|
|
oklch(91% 0.06 175) 30deg 60deg,
|
|
oklch(91% 0.06 245) 60deg 90deg,
|
|
oklch(91% 0.07 345) 90deg 120deg,
|
|
oklch(91% 0.06 205) 120deg 150deg,
|
|
oklch(91% 0.06 175) 150deg 180deg,
|
|
oklch(91% 0.06 245) 180deg 210deg,
|
|
oklch(91% 0.07 345) 210deg 240deg,
|
|
oklch(91% 0.06 205) 240deg 270deg,
|
|
oklch(91% 0.06 175) 270deg 300deg,
|
|
oklch(91% 0.06 245) 300deg 330deg,
|
|
oklch(91% 0.07 345) 330deg 360deg
|
|
);
|
|
}
|
|
|
|
.clock-sections-gaps {
|
|
--d: 55.6vmin;
|
|
background: repeating-conic-gradient(
|
|
from 14.6deg,
|
|
oklch(99% 0.005 210) 0deg 0.8deg,
|
|
transparent 0.8deg 30deg
|
|
);
|
|
}
|
|
|
|
.clock-sections-rim {
|
|
--d: 55.6vmin;
|
|
border: 0.4vmin solid var(--clock-ink);
|
|
}
|
|
|
|
.clock-center {
|
|
--d: 20vmin;
|
|
background: oklch(99% 0.005 210);
|
|
box-shadow: 0 0 0 0.4vmin oklch(28% 0.04 220 / 0.25);
|
|
}
|
|
|
|
.clock-minute-track {
|
|
--d: 66.8vmin;
|
|
background: conic-gradient(
|
|
from -15deg,
|
|
oklch(80% 0.1 205) 0deg 30deg,
|
|
oklch(80% 0.1 175) 30deg 60deg,
|
|
oklch(80% 0.1 245) 60deg 90deg,
|
|
oklch(80% 0.13 345) 90deg 120deg,
|
|
oklch(80% 0.1 205) 120deg 150deg,
|
|
oklch(80% 0.1 175) 150deg 180deg,
|
|
oklch(80% 0.1 245) 180deg 210deg,
|
|
oklch(80% 0.13 345) 210deg 240deg,
|
|
oklch(80% 0.1 205) 240deg 270deg,
|
|
oklch(80% 0.1 175) 270deg 300deg,
|
|
oklch(80% 0.1 245) 300deg 330deg,
|
|
oklch(80% 0.13 345) 330deg 360deg
|
|
);
|
|
mask-image: radial-gradient(circle closest-side, transparent 82.8%, #000 83.2%);
|
|
}
|
|
|
|
.clock-minute-ticks {
|
|
--d: 66.8vmin;
|
|
background: repeating-conic-gradient(
|
|
from -0.5deg,
|
|
var(--clock-ink) 0deg 1deg,
|
|
transparent 1deg 6deg
|
|
);
|
|
mask-image: radial-gradient(circle closest-side, transparent 94.6%, #000 95%);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.clock-minute-ticks-major {
|
|
--d: 66.8vmin;
|
|
background: repeating-conic-gradient(
|
|
from -1.3deg,
|
|
var(--clock-ink) 0deg 2.6deg,
|
|
transparent 2.6deg 30deg
|
|
);
|
|
mask-image: radial-gradient(circle closest-side, transparent 93.2%, #000 93.6%);
|
|
}
|
|
|
|
.clock-numbers {
|
|
inset: 0;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
}
|
|
|
|
.clock-numbers > div {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
}
|
|
|
|
.clock-numbers-minutes {
|
|
font-size: 2.3vmin;
|
|
color: var(--clock-ink);
|
|
}
|
|
|
|
.clock-numbers-hours {
|
|
font-size: 8.2vmin;
|
|
letter-spacing: -0.3vmin;
|
|
-webkit-text-stroke: 0.45vmin var(--clock-ink);
|
|
paint-order: stroke fill;
|
|
}
|
|
|
|
.clock-numbers-hours > [data-hue="205"] {
|
|
color: oklch(56% 0.13 205);
|
|
}
|
|
|
|
.clock-numbers-hours > [data-hue="175"] {
|
|
color: oklch(56% 0.13 175);
|
|
}
|
|
|
|
.clock-numbers-hours > [data-hue="245"] {
|
|
color: oklch(56% 0.13 245);
|
|
}
|
|
|
|
.clock-numbers-hours > [data-hue="pink"] {
|
|
color: oklch(60% 0.19 345);
|
|
}
|
|
|
|
/* A hand is a box standing on the centre, rotated about its foot. */
|
|
.clock-hand {
|
|
left: 50%;
|
|
bottom: 50%;
|
|
transform-origin: 50% 100%;
|
|
/* Each hand on its own layer: turning it moves a finished picture on the GPU,
|
|
and the dial underneath is never repainted. */
|
|
will-change: transform;
|
|
}
|
|
|
|
/* The outline and shadow, on the hand's inner box rather than the hand: a filter on the
|
|
layer itself would be recomputed on the GPU every frame anything on screen moves (the
|
|
bubbles), while one inside it is drawn once into the layer's picture. The shape is
|
|
one level further in, so the outline goes round the clipped arrow, not its box. */
|
|
.clock-hand-hour > div,
|
|
.clock-hand-minute > div {
|
|
filter: drop-shadow(0.35vmin 0 0 var(--clock-ink)) drop-shadow(-0.35vmin 0 0 var(--clock-ink))
|
|
drop-shadow(0 0.35vmin 0 var(--clock-ink)) drop-shadow(0 -0.35vmin 0 var(--clock-ink))
|
|
drop-shadow(0 1vmin 1vmin oklch(15% 0.05 210 / 0.3));
|
|
}
|
|
|
|
.clock-hand > div {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: 0;
|
|
}
|
|
|
|
.clock-hand-hour {
|
|
width: 5.4vmin;
|
|
height: 15vmin;
|
|
margin-left: -2.7vmin;
|
|
}
|
|
|
|
.clock-hand-hour > div,
|
|
.clock-hand-minute > div {
|
|
bottom: -3.5vmin;
|
|
}
|
|
|
|
.clock-hand-hour > div > div,
|
|
.clock-hand-minute > div > div {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
.clock-hand-hour > div > div {
|
|
background: oklch(92% 0.06 345);
|
|
clip-path: polygon(50% 0%, 100% 22%, 66% 22%, 66% 100%, 34% 100%, 34% 22%, 0% 22%);
|
|
}
|
|
|
|
.clock-hand-minute {
|
|
width: 5vmin;
|
|
height: 24vmin;
|
|
margin-left: -2.5vmin;
|
|
}
|
|
|
|
.clock-hand-minute > div > div {
|
|
background: oklch(92% 0.05 200);
|
|
clip-path: polygon(50% 0%, 100% 14%, 64% 14%, 64% 100%, 36% 100%, 36% 14%, 0% 14%);
|
|
}
|
|
|
|
.clock-hand-second {
|
|
width: 0.7vmin;
|
|
height: 27vmin;
|
|
margin-left: -0.35vmin;
|
|
}
|
|
|
|
.clock-hand-second > div {
|
|
bottom: -5vmin;
|
|
border-radius: 1vmin;
|
|
background: oklch(64% 0.19 345);
|
|
}
|
|
|
|
.clock-pin {
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 4.2vmin;
|
|
height: 4.2vmin;
|
|
margin: -2.1vmin 0 0 -2.1vmin;
|
|
border-radius: 50%;
|
|
background: oklch(80% 0.1 205);
|
|
box-shadow: 0 0 0 0.5vmin var(--clock-ink);
|
|
}
|
|
|
|
.clock-pin-dot {
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 1.4vmin;
|
|
height: 1.4vmin;
|
|
margin: -0.7vmin 0 0 -0.7vmin;
|
|
border-radius: 50%;
|
|
background: var(--clock-ink);
|
|
}
|
|
|
|
/* -------------------------------------------------------------- the countdowns */
|
|
|
|
.clock-countdowns {
|
|
position: relative;
|
|
flex: 0 1 74vmin;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3vmin;
|
|
}
|
|
|
|
@media (orientation: portrait) {
|
|
.clock-countdowns {
|
|
flex: none;
|
|
width: 88vw;
|
|
}
|
|
}
|
|
|
|
.clock-card {
|
|
--clock-bar: oklch(66% 0.12 205);
|
|
--clock-time: oklch(30% 0.05 210);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3.5vmin;
|
|
padding: 3vmin 4vmin 3vmin 3vmin;
|
|
border-radius: 4.5vmin;
|
|
background: oklch(97% 0.01 210 / 0.68);
|
|
backdrop-filter: blur(10px);
|
|
box-shadow: 0 2vmin 5vmin oklch(15% 0.05 210 / 0.35);
|
|
}
|
|
|
|
[data-blur="off"] .clock-card {
|
|
backdrop-filter: none;
|
|
background: oklch(97% 0.01 210 / 0.88);
|
|
}
|
|
|
|
.clock-card[data-soon] {
|
|
--clock-bar: var(--clock-pink);
|
|
--clock-time: oklch(58% 0.19 345);
|
|
}
|
|
|
|
.clock-card[data-ringing] {
|
|
animation: clockRing 1.2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes clockRing {
|
|
0%,
|
|
100% {
|
|
background: oklch(97% 0.01 210 / 0.68);
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
background: oklch(88% 0.09 345 / 0.92);
|
|
transform: scale(1.03);
|
|
}
|
|
}
|
|
|
|
.clock-card-image {
|
|
width: 24vmin;
|
|
height: 18vmin;
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.clock-card-image > img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
filter: drop-shadow(0 1vmin 1.5vmin oklch(15% 0.05 210 / 0.3));
|
|
}
|
|
|
|
.clock-card-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.6vmin;
|
|
}
|
|
|
|
.clock-card-time {
|
|
font-size: 15vmin;
|
|
font-weight: 900;
|
|
line-height: 0.95;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: -0.3vmin;
|
|
color: var(--clock-time);
|
|
}
|
|
|
|
.clock-card-bar {
|
|
height: 2vmin;
|
|
margin-top: 0.8vmin;
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
background: oklch(30% 0.04 210 / 0.15);
|
|
}
|
|
|
|
.clock-card-bar > div {
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
background: var(--clock-bar);
|
|
transition: width 0.25s linear;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.clock-card[data-ringing] {
|
|
animation: none;
|
|
background: oklch(88% 0.09 345 / 0.92);
|
|
}
|
|
}
|