Web frontend: switchable dolphin/dinosaur skins
Two kids, two looks: ?theme=dolphin or ?theme=dinosaur picks one, and the choice is remembered in the browser so the installed home-screen app keeps it. Everything that differs per child lives in one Skin object per file under web/src/lib/skins/; layout and feature toggles in lib/theme.ts stay shared. The dolphin renders pixel-identical to before. The hardcoded sea hues (210 music, 175 typing game, 340 accent) are now CSS variables written once at startup, and the Gecko-friendly sRGB gradient stops come from the skin. The dinosaur is jungle green with a lava-orange accent, its own mascot, icons, manifest and browser colour. Nothing underwater is left in the dinosaur skin: leaves instead of bubbles (play-view canvas, room page, typing game), a walking mascot, lava blobs in the bubble game, a hatching egg instead of the treasure chest, dino pets that walk or fly, a prehistoric speed ladder with the T-Rex as the hero rung, and renamed games and light colours. Pets and ladder rungs are keyed by the backend's existing ids, used as slots, so the backend and save format are unchanged. Pets show as emoji until drawings exist. python-backend/.gitignore: ignore tippen-curriculum-dino.yml, the dino device's personal curriculum, like the original. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
the tokens below are the handful of values that repeat. */
|
||||
|
||||
:root {
|
||||
--ink: oklch(30% 0.04 210);
|
||||
--paper: oklch(97% 0.01 210);
|
||||
--accent: oklch(70% 0.16 340);
|
||||
--accent-dim: oklch(78% 0.14 340);
|
||||
--sea-deep: oklch(20% 0.045 210);
|
||||
--shadow: oklch(15% 0.05 210 / 0.35);
|
||||
--ink: oklch(30% 0.04 var(--hue));
|
||||
--paper: oklch(97% 0.01 var(--hue));
|
||||
--accent: oklch(70% 0.16 var(--accent-hue));
|
||||
--accent-dim: oklch(78% 0.14 var(--accent-hue));
|
||||
--sea-deep: oklch(20% 0.045 var(--hue));
|
||||
--shadow: oklch(15% 0.05 var(--hue) / 0.35);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -48,7 +48,7 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dolphinBob {
|
||||
@keyframes mascotBob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) rotate(-2deg);
|
||||
@@ -58,7 +58,7 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dolphinSwim {
|
||||
@keyframes mascotSwim {
|
||||
0% { transform: translate(-20vw, 10vh) rotate(-3deg) scaleX(1); }
|
||||
15% { transform: translate(2vw, 2vh) rotate(4deg) scaleX(1); }
|
||||
30% { transform: translate(24vw, 12vh) rotate(-3deg) scaleX(1); }
|
||||
@@ -71,6 +71,21 @@ button {
|
||||
100% { transform: translate(-20vw, 10vh) rotate(-3deg) scaleX(-1); }
|
||||
}
|
||||
|
||||
/* The walking mascot's crossing: the same timeline as `mascotSwim`, turn included, but
|
||||
along one level line instead of in waves. `mascotWaddle` runs beside it on the
|
||||
separate `rotate`/`translate` properties, which compose with this `transform`. */
|
||||
@keyframes mascotWalk {
|
||||
0% { transform: translate(-20vw, 10vh) scaleX(1); }
|
||||
70% { transform: translate(84vw, 10vh) scaleX(1); }
|
||||
74% { transform: translate(84vw, 10vh) scaleX(-1); }
|
||||
100% { transform: translate(-20vw, 10vh) scaleX(-1); }
|
||||
}
|
||||
|
||||
@keyframes mascotWaddle {
|
||||
0%, 100% { rotate: -4deg; translate: 0 0; }
|
||||
50% { rotate: 4deg; translate: 0 -6px; }
|
||||
}
|
||||
|
||||
/* Entrance animations for the browse grid and the album modal - gated by
|
||||
`ANIMATE_VIEW_TRANSITIONS` in `lib/theme.ts`. */
|
||||
@keyframes viewEnter {
|
||||
@@ -106,9 +121,10 @@ button {
|
||||
overflow: hidden;
|
||||
/* Gradient stops are plain sRGB hex, not oklch(): a gradient with an oklch stop
|
||||
interpolates in Oklab, which Gecko expands stop by stop on the CPU every time the
|
||||
display list is rebuilt - about 15% of a Pi-profile's busy time. Values are the
|
||||
oklch(55% 0.07 210) / (38% 0.06 210) / (20% 0.045 210) they replace. */
|
||||
background: linear-gradient(180deg, #397d88 0%, #0e4b54 45%, #001b21 100%);
|
||||
display list is rebuilt - about 15% of a Pi-profile's busy time. The skin supplies
|
||||
them (`stageGradient` in lib/skins/), pre-converted from oklch(55% 0.07 hue) /
|
||||
(38% 0.06 hue) / (20% 0.045 hue). */
|
||||
background: linear-gradient(180deg, var(--stage-top) 0%, var(--stage-mid) 45%, var(--stage-deep) 100%);
|
||||
}
|
||||
|
||||
/* Still used by the room-control page's own `<Bubbles>` field (`RoomView.tsx`) - the
|
||||
@@ -124,6 +140,26 @@ button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* A leaf (Drift.tsx, the dinosaur's answer to `.bubble`) tumbling down the stage and
|
||||
swaying on its way. Transform only: each leaf's opacity is its own inline style. */
|
||||
@keyframes leafFall {
|
||||
0% { transform: translate(0, 0) rotate(0deg); }
|
||||
25% { transform: translate(28px, 32vh) rotate(80deg); }
|
||||
50% { transform: translate(-6px, 64vh) rotate(170deg); }
|
||||
75% { transform: translate(24px, 96vh) rotate(250deg); }
|
||||
100% { transform: translate(-10px, calc(100vh + 80px)) rotate(330deg); }
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
line-height: 1;
|
||||
animation-name: leafFall;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ambience {
|
||||
display: block;
|
||||
/* A canvas is a replaced element: without an explicit size here, its CSS layout box
|
||||
@@ -146,13 +182,13 @@ button {
|
||||
padding: 9px 15px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
background: oklch(97% 0.01 210 / 0.16);
|
||||
background: oklch(97% 0.01 var(--hue) / 0.16);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.pill[data-active="true"] {
|
||||
background: var(--paper);
|
||||
color: oklch(28% 0.04 210);
|
||||
color: oklch(28% 0.04 var(--hue));
|
||||
box-shadow: 0 4px 14px var(--shadow);
|
||||
}
|
||||
|
||||
@@ -269,10 +305,10 @@ button {
|
||||
flex: none;
|
||||
border-radius: 999px;
|
||||
font-size: 17px;
|
||||
background: linear-gradient(160deg, #eef7f961, #eef7f91f) /* oklch(97% 0.01 210 / .38 -> .12) */;
|
||||
background: linear-gradient(160deg, rgb(var(--paper-rgb) / .38), rgb(var(--paper-rgb) / .12)) /* oklch(97% 0.01 hue), kept sRGB like .stage */;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid oklch(97% 0.01 210 / 0.4);
|
||||
border: 1px solid oklch(97% 0.01 var(--hue) / 0.4);
|
||||
box-shadow:
|
||||
0 3px 10px var(--shadow),
|
||||
inset 0 1px 0 oklch(100% 0 0 / 0.35);
|
||||
@@ -281,10 +317,10 @@ button {
|
||||
/* A frosted panel wrapped around a shelf/list of rows so they read as a distinct
|
||||
surface floating over the stage gradient, rather than flat text on a gradient. */
|
||||
.glass-panel {
|
||||
background: linear-gradient(160deg, #eef7f924, #eef7f90d) /* oklch(97% 0.01 210 / .14 -> .05) */;
|
||||
background: linear-gradient(160deg, rgb(var(--paper-rgb) / .14), rgb(var(--paper-rgb) / .05)) /* oklch(97% 0.01 hue), kept sRGB like .stage */;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid oklch(97% 0.01 210 / 0.16);
|
||||
border: 1px solid oklch(97% 0.01 var(--hue) / 0.16);
|
||||
border-radius: 22px;
|
||||
box-shadow:
|
||||
0 8px 24px var(--shadow),
|
||||
@@ -296,7 +332,7 @@ button {
|
||||
.list-row {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid oklch(97% 0.01 210 / 0.16);
|
||||
border: 1px solid oklch(97% 0.01 var(--hue) / 0.16);
|
||||
box-shadow:
|
||||
0 4px 14px var(--shadow),
|
||||
inset 0 1px 0 oklch(100% 0 0 / 0.12);
|
||||
@@ -326,13 +362,13 @@ button {
|
||||
.sheet {
|
||||
background: var(--paper);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 50px oklch(10% 0.04 210 / 0.5);
|
||||
box-shadow: 0 20px 50px oklch(10% 0.04 var(--hue) / 0.5);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: oklch(15% 0.03 210 / 0.55);
|
||||
background: oklch(15% 0.03 var(--hue) / 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -398,7 +434,7 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.searchbox-placeholder {
|
||||
color: oklch(30% 0.04 210 / .45);
|
||||
color: oklch(30% 0.04 var(--hue) / .45);
|
||||
}
|
||||
|
||||
.searchbox-caret {
|
||||
@@ -417,7 +453,7 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.searchbox-note {
|
||||
color: oklch(90% 0.02 210 / .85);
|
||||
color: oklch(90% 0.02 var(--hue) / .85);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
/* 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 210 / 0.42);
|
||||
--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 210 / 0.45);
|
||||
--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));
|
||||
@@ -73,7 +73,7 @@ body {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 3px 8px oklch(15% 0.05 210 / 0.5));
|
||||
filter: drop-shadow(0 3px 8px oklch(15% 0.05 var(--hue) / 0.5));
|
||||
}
|
||||
|
||||
.m-title {
|
||||
@@ -82,14 +82,14 @@ body {
|
||||
font-weight: 900;
|
||||
color: var(--paper);
|
||||
letter-spacing: -0.01em;
|
||||
text-shadow: 0 2px 10px oklch(15% 0.05 210 / 0.45);
|
||||
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 210 / 0.16);
|
||||
background: oklch(97% 0.01 var(--hue) / 0.16);
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
@@ -157,7 +157,7 @@ body {
|
||||
margin: -6px var(--m-gutter) 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: oklch(90% 0.02 210 / 0.75);
|
||||
color: oklch(90% 0.02 var(--hue) / 0.75);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- segmented -- */
|
||||
@@ -167,11 +167,11 @@ body {
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border-radius: 999px;
|
||||
background: oklch(20% 0.04 210 / 0.38);
|
||||
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 210 / 0.35);
|
||||
box-shadow: inset 0 1px 2px oklch(10% 0.04 var(--hue) / 0.35);
|
||||
}
|
||||
|
||||
.m-segment {
|
||||
@@ -179,7 +179,7 @@ body {
|
||||
min-width: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: oklch(95% 0.01 210 / 0.85);
|
||||
color: oklch(95% 0.01 var(--hue) / 0.85);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
@@ -195,7 +195,7 @@ body {
|
||||
.m-segment[data-active="true"] {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
box-shadow: 0 3px 10px oklch(10% 0.04 210 / 0.35);
|
||||
box-shadow: 0 3px 10px oklch(10% 0.04 var(--hue) / 0.35);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ grids -- */
|
||||
@@ -243,7 +243,7 @@ body {
|
||||
}
|
||||
|
||||
.m-album-cover {
|
||||
box-shadow: 0 6px 16px oklch(10% 0.04 210 / 0.45);
|
||||
box-shadow: 0 6px 16px oklch(10% 0.04 var(--hue) / 0.45);
|
||||
transition: transform 0.12s ease;
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ body {
|
||||
|
||||
.m-song:active,
|
||||
.m-song[data-current="true"] {
|
||||
background: oklch(97% 0.01 210 / 0.14);
|
||||
background: oklch(97% 0.01 var(--hue) / 0.14);
|
||||
}
|
||||
|
||||
.m-song-cover {
|
||||
@@ -323,12 +323,12 @@ body {
|
||||
.m-song-sub {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: oklch(88% 0.02 210 / 0.65);
|
||||
color: oklch(88% 0.02 var(--hue) / 0.65);
|
||||
}
|
||||
|
||||
.m-song-time {
|
||||
font: 700 12px ui-monospace, Menlo, monospace;
|
||||
color: oklch(88% 0.02 210 / 0.6);
|
||||
color: oklch(88% 0.02 var(--hue) / 0.6);
|
||||
}
|
||||
|
||||
.m-hint {
|
||||
@@ -337,7 +337,7 @@ body {
|
||||
padding: 0 var(--m-gutter);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: oklch(92% 0.02 210 / 0.8);
|
||||
color: oklch(92% 0.02 var(--hue) / 0.8);
|
||||
}
|
||||
|
||||
.m-hint img {
|
||||
@@ -422,14 +422,14 @@ body {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: oklch(95% 0.01 210 / 0.8);
|
||||
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 210 / 0.2);
|
||||
background: oklch(97% 0.01 var(--hue) / 0.2);
|
||||
color: var(--paper);
|
||||
box-shadow: inset 0 1px 0 oklch(100% 0 0 / 0.2);
|
||||
}
|
||||
@@ -483,7 +483,7 @@ body {
|
||||
}
|
||||
|
||||
.m-search input::placeholder {
|
||||
color: oklch(95% 0.01 210 / 0.55);
|
||||
color: oklch(95% 0.01 var(--hue) / 0.55);
|
||||
}
|
||||
|
||||
.m-search input::-webkit-search-cancel-button {
|
||||
@@ -494,7 +494,7 @@ body {
|
||||
cursor: pointer;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: oklch(95% 0.01 210 / 0.7);
|
||||
color: oklch(95% 0.01 var(--hue) / 0.7);
|
||||
}
|
||||
|
||||
.m-search-clear {
|
||||
@@ -506,7 +506,7 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: oklch(97% 0.01 210 / 0.3);
|
||||
background: oklch(97% 0.01 var(--hue) / 0.3);
|
||||
color: var(--sea-deep);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
@@ -545,7 +545,7 @@ body {
|
||||
height: 44px;
|
||||
flex: none;
|
||||
display: flex;
|
||||
box-shadow: 0 3px 10px oklch(10% 0.04 210 / 0.4);
|
||||
box-shadow: 0 3px 10px oklch(10% 0.04 var(--hue) / 0.4);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ body {
|
||||
.m-mini-sub {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: oklch(90% 0.02 210 / 0.7);
|
||||
color: oklch(90% 0.02 var(--hue) / 0.7);
|
||||
}
|
||||
|
||||
.m-mini-button {
|
||||
@@ -587,7 +587,7 @@ body {
|
||||
}
|
||||
|
||||
.m-mini-button:active {
|
||||
background: oklch(97% 0.01 210 / 0.15);
|
||||
background: oklch(97% 0.01 var(--hue) / 0.15);
|
||||
}
|
||||
|
||||
.m-mini-progress {
|
||||
|
||||
@@ -3,24 +3,25 @@
|
||||
the radius scale and the entrance animations are deliberately identical [to
|
||||
app.css], so the two apps read as siblings. What changes is the hue: the music
|
||||
player's sea is 210, this is 175 - a turquoise lagoon, clearly the same ocean and
|
||||
clearly not the same room.").
|
||||
clearly not the same room."). Both hues now come from the skin (lib/skins/) as
|
||||
`--hue` and `--tp-hue`; 210/175 are the dolphin's.
|
||||
|
||||
Classes here are prefixed `tp-` and colours scoped under `.tp-stage` rather than
|
||||
redefined at `:root`, because app.css already claims the *same* custom property
|
||||
names (--ink, --paper, --accent, --shadow, ...) for the music player's own blue hue -
|
||||
redefining them globally would have silently reskinned one app or the other. Anything
|
||||
below with no hue baked into it - .bubble, .card, .key-cap, .view-enter,
|
||||
.backdrop-enter, and the dolphinBob/bubbleRise/viewEnter/backdropEnter keyframes - is
|
||||
.backdrop-enter, and the mascotBob/bubbleRise/viewEnter/backdropEnter keyframes - is
|
||||
identical between the two apps and reused directly from app.css instead of being
|
||||
duplicated here. */
|
||||
|
||||
.tp-stage {
|
||||
--ink: oklch(30% 0.04 175);
|
||||
--paper: oklch(97% 0.01 175);
|
||||
--accent: oklch(70% 0.16 340);
|
||||
--accent-dim: oklch(78% 0.14 340);
|
||||
--sea-deep: oklch(20% 0.045 175);
|
||||
--shadow: oklch(15% 0.05 175 / 0.35);
|
||||
--ink: oklch(30% 0.04 var(--tp-hue));
|
||||
--paper: oklch(97% 0.01 var(--tp-hue));
|
||||
--accent: oklch(70% 0.16 var(--accent-hue));
|
||||
--accent-dim: oklch(78% 0.14 var(--accent-hue));
|
||||
--sea-deep: oklch(20% 0.045 var(--tp-hue));
|
||||
--shadow: oklch(15% 0.05 var(--tp-hue) / 0.35);
|
||||
/* This app's own additions: the feedback colours. Note there is no red - a wrong key
|
||||
is amber and gentle, never an alarm. */
|
||||
--correct: oklch(80% 0.17 150);
|
||||
@@ -32,8 +33,8 @@
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
oklch(55% 0.07 175) 0%,
|
||||
oklch(38% 0.06 175) 45%,
|
||||
oklch(55% 0.07 var(--tp-hue)) 0%,
|
||||
oklch(38% 0.06 var(--tp-hue)) 45%,
|
||||
var(--sea-deep) 100%
|
||||
);
|
||||
display: flex;
|
||||
@@ -51,8 +52,8 @@
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* None of these five collide with app.css's own keyframes (bubbleRise, dolphinBob,
|
||||
dolphinSwim, viewEnter, sheetEnter, backdropEnter), so they keep their plain names -
|
||||
/* None of these five collide with app.css's own keyframes (bubbleRise, mascotBob,
|
||||
mascotSwim, viewEnter, sheetEnter, backdropEnter), so they keep their plain names -
|
||||
only classes that would otherwise collide are `tp-`-prefixed. */
|
||||
@keyframes correctPop {
|
||||
0% { transform: scale(1); }
|
||||
@@ -69,8 +70,8 @@
|
||||
}
|
||||
|
||||
@keyframes keyPulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 oklch(97% 0.01 175 / 0.6); }
|
||||
50% { box-shadow: 0 0 0 9px oklch(97% 0.01 175 / 0); }
|
||||
0%, 100% { box-shadow: 0 0 0 0 oklch(97% 0.01 var(--tp-hue) / 0.6); }
|
||||
50% { box-shadow: 0 0 0 9px oklch(97% 0.01 var(--tp-hue) / 0); }
|
||||
}
|
||||
|
||||
@keyframes tierEnter {
|
||||
@@ -93,6 +94,7 @@
|
||||
/* Keep the colour feedback, drop the movement - covers the shared .bubble/.view-enter/
|
||||
.backdrop-enter classes too, which app.css does not otherwise handle. */
|
||||
.bubble,
|
||||
.leaf,
|
||||
.view-enter,
|
||||
.tp-sheet-enter,
|
||||
.backdrop-enter {
|
||||
@@ -101,10 +103,10 @@
|
||||
}
|
||||
|
||||
.tp-glass-panel {
|
||||
background: linear-gradient(160deg, oklch(97% 0.01 175 / 0.14), oklch(97% 0.01 175 / 0.05));
|
||||
background: linear-gradient(160deg, oklch(97% 0.01 var(--tp-hue) / 0.14), oklch(97% 0.01 var(--tp-hue) / 0.05));
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid oklch(97% 0.01 175 / 0.16);
|
||||
border: 1px solid oklch(97% 0.01 var(--tp-hue) / 0.16);
|
||||
border-radius: 22px;
|
||||
box-shadow:
|
||||
0 8px 24px var(--shadow),
|
||||
@@ -126,26 +128,26 @@
|
||||
padding: 9px 15px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
background: oklch(97% 0.01 175 / 0.16);
|
||||
background: oklch(97% 0.01 var(--tp-hue) / 0.16);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.tp-pill[data-active="true"] {
|
||||
background: var(--paper);
|
||||
color: oklch(28% 0.04 175);
|
||||
color: oklch(28% 0.04 var(--tp-hue));
|
||||
box-shadow: 0 4px 14px var(--shadow);
|
||||
}
|
||||
|
||||
.tp-sheet {
|
||||
background: var(--paper);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 50px oklch(10% 0.04 175 / 0.5);
|
||||
box-shadow: 0 20px 50px oklch(10% 0.04 var(--tp-hue) / 0.5);
|
||||
}
|
||||
|
||||
.tp-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: oklch(15% 0.03 175 / 0.55);
|
||||
background: oklch(15% 0.03 var(--tp-hue) / 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -179,8 +181,8 @@
|
||||
font-size: calc(var(--kb-size, 42px) * 0.42);
|
||||
font-weight: 800;
|
||||
color: var(--paper);
|
||||
background: oklch(97% 0.01 175 / 0.1);
|
||||
border: 1px solid oklch(97% 0.01 175 / 0.14);
|
||||
background: oklch(97% 0.01 var(--tp-hue) / 0.1);
|
||||
border: 1px solid oklch(97% 0.01 var(--tp-hue) / 0.14);
|
||||
backdrop-filter: blur(6px);
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
@@ -209,7 +211,7 @@
|
||||
|
||||
.tp-kb-key[data-next="true"] {
|
||||
background: var(--paper);
|
||||
color: oklch(25% 0.05 175);
|
||||
color: oklch(25% 0.05 var(--tp-hue));
|
||||
transform: translateY(-3px) scale(1.08);
|
||||
animation: keyPulse 1.3s ease-out infinite;
|
||||
}
|
||||
@@ -240,7 +242,7 @@
|
||||
they read as the modifier they are, and dim until a capital actually needs one. */
|
||||
.tp-kb-shift {
|
||||
width: calc(var(--kb-size, 42px) * 1.8);
|
||||
background: oklch(97% 0.01 175 / 0.12);
|
||||
background: oklch(97% 0.01 var(--tp-hue) / 0.12);
|
||||
font-size: calc(var(--kb-size, 42px) * 0.4);
|
||||
}
|
||||
|
||||
@@ -273,7 +275,7 @@
|
||||
wider, the whole line would reflow around the cursor as she types, and chunks
|
||||
would visibly jump between lines mid-word. */
|
||||
padding: 0 6px;
|
||||
color: oklch(97% 0.01 175 / 0.45);
|
||||
color: oklch(97% 0.01 var(--tp-hue) / 0.45);
|
||||
transition: color 0.12s ease;
|
||||
}
|
||||
|
||||
@@ -282,7 +284,7 @@
|
||||
}
|
||||
|
||||
.tp-target-char[data-state="current"] {
|
||||
color: oklch(25% 0.05 175);
|
||||
color: oklch(25% 0.05 var(--tp-hue));
|
||||
background: var(--paper);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 18px var(--shadow);
|
||||
@@ -342,19 +344,20 @@
|
||||
overflow: hidden;
|
||||
background: radial-gradient(
|
||||
circle at 50% 42%,
|
||||
oklch(45% 0.07 175 / 0.82) 0%,
|
||||
oklch(16% 0.04 175 / 0.94) 70%
|
||||
oklch(45% 0.07 var(--tp-hue) / 0.82) 0%,
|
||||
oklch(16% 0.04 var(--tp-hue) / 0.94) 70%
|
||||
);
|
||||
animation: backdropEnter 260ms ease-out;
|
||||
/* Thrown right back, out of the column the cover rises through - see the note on
|
||||
.tp-chest-lid. Alone this would look like a lid coming off; with the cover standing
|
||||
.tp-reward-lid. Alone this would look like a lid coming off; with the cover standing
|
||||
in the middle it reads as the lid flung open to make room for it. */
|
||||
--tp-chest-open: -108deg;
|
||||
--tp-reward-open: -108deg;
|
||||
}
|
||||
|
||||
/* The lid's two states. Transitioned rather than keyframed so the map can draw a
|
||||
/* The lid's two states - the chest's lid, or the egg's top shell (DinoEgg.tsx), which
|
||||
hinges the same way. Transitioned rather than keyframed so the map can draw a
|
||||
permanently open chest with no animation at all - see TreasureChest.tsx. */
|
||||
.tp-chest-lid {
|
||||
.tp-reward-lid {
|
||||
transform-box: view-box;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 620ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
@@ -365,12 +368,12 @@
|
||||
attached to its hinge - a lid much past -90deg, alone, reads as a flap flying off.
|
||||
The overlay overrides it above, because there the lid has a cover rising through the
|
||||
middle of the chest and has to clear out of its way. */
|
||||
.tp-chest-lid[data-open] {
|
||||
transform: rotate(var(--tp-chest-open, -62deg));
|
||||
.tp-reward-lid[data-open] {
|
||||
transform: rotate(var(--tp-reward-open, -62deg));
|
||||
}
|
||||
|
||||
/* The chest arriving: dropped in, with a squash on landing. */
|
||||
@keyframes chestDrop {
|
||||
@keyframes rewardDrop {
|
||||
0% { opacity: 0; transform: translateY(-120px) scale(0.7); }
|
||||
55% { opacity: 1; transform: translateY(0) scale(1.06, 0.9); }
|
||||
75% { transform: translateY(-6px) scale(0.98, 1.04); }
|
||||
@@ -380,7 +383,7 @@
|
||||
/* The wait before the lid goes: the chest rattling, because something inside wants out.
|
||||
This is the beat that earns the opening - a lid that opens the instant the chest lands
|
||||
is just a transition, not a reveal. */
|
||||
@keyframes chestRattle {
|
||||
@keyframes rewardRattle {
|
||||
0%, 100% { transform: translateX(0) rotate(0deg); }
|
||||
15% { transform: translateX(-3px) rotate(-2.5deg); }
|
||||
30% { transform: translateX(3px) rotate(2.5deg); }
|
||||
@@ -447,7 +450,7 @@
|
||||
.tp-reward-overlay * {
|
||||
animation: none !important;
|
||||
}
|
||||
.tp-chest-lid {
|
||||
.tp-reward-lid {
|
||||
transition: none;
|
||||
}
|
||||
.tp-confetti-piece {
|
||||
|
||||
Reference in New Issue
Block a user