feat: shine a code redeemed or an event completed on its way out

A code redeemed on its last server sweeps in the game's colour and greys
in place for 0.8 s, then lands in Redeemed or expired. A completed event
greys and shines while its Moved to Archive notice holds, now 0.9 s
rather than 1.5 s, which read as unresponsive; an ignored one only greys.

Settings → Appearance gains Motion & celebrations as three What to track
cards: Reduce motion (stops every animation and the fireworks, adding to
the device's own setting), Fireworks, and Shine. Reduce motion locks the
other two off without forgetting them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-20 04:26:50 +02:00
1 parent 9c22a85134
commit 54d13d7472
15 files changed
+628 -59

No files matched your search

+112 -12
View File
@@ -4268,6 +4268,86 @@ dialog.archive-detail {
text-decoration: line-through;
text-decoration-color: var(--color-muted);
}
/* A checklist event on its way to the Archive greys its text while it holds,
so it reads as leaving; the "Moved to Archive" line under it stays legible. */
.event-list > li[data-leaving] > button > span {
opacity: 0.45;
}
/* A code shining on its way to Redeemed greys the same way; its Redeemed
boxes stay full strength, so the tick that sent it is still plain. */
.code-row[data-shine] > div:first-child > * {
opacity: 0.45;
}
/* A code just redeemed, or an event just completed: the dailies' sweep and
ring cross the row in the game's colour. A code then fades out and lands
among the redeemed; an event fades as the Archive takes it. Only the
reader's own action sets these, and reduced motion skips the code's hold. */
@media (prefers-reduced-motion: no-preference) {
.event-list > li > button > span,
.code-row > div:first-child > * {
transition: opacity 300ms ease-out;
}
.code-row[data-shine],
.event-list > li[data-shine] > button {
position: relative;
animation: daily-glow 560ms ease-out;
}
.code-row[data-shine] {
animation:
daily-glow 560ms ease-out,
code-leave 210ms ease-in 590ms forwards;
}
.code-row[data-shine]::after,
.event-list > li[data-shine] > button::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
100deg,
transparent 30%,
color-mix(in srgb, var(--g, var(--color-ink)) 38%, transparent) 50%,
transparent 70%
);
background-size: 250% 100%;
animation: code-sweep 640ms ease-out both;
}
.code-row[data-landing] {
animation: code-land 360ms ease-out;
}
.code-row label[data-fresh] input {
animation: daily-pop 420ms cubic-bezier(0.3, 1.6, 0.5, 1);
}
.codes-clear[data-fresh] {
animation: daily-rise 360ms ease-out;
}
/* The dailies' sweep, held bright until it has crossed: a single row is
too thin for a band that fades as it travels. */
@keyframes code-sweep {
from {
background-position: 100% 0;
}
75% {
opacity: 1;
}
to {
background-position: 0% 0;
opacity: 0;
}
}
@keyframes code-leave {
to {
opacity: 0;
transform: translateY(6px);
}
}
@keyframes code-land {
from {
opacity: 0;
transform: translateY(-6px);
}
}
}
.game-settings .new-code {
margin-left: 6px;
font-size: 11px;
@@ -4292,13 +4372,13 @@ dialog.archive-detail {
color: var(--color-faint);
letter-spacing: 0;
}
.game-settings .comp {
:is(.game-settings, .motion-cards) .comp {
margin: 14px 0 8px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.game-settings .comp button {
:is(.game-settings, .motion-cards) .comp button {
position: relative;
display: flex;
flex-direction: column;
@@ -4308,22 +4388,22 @@ dialog.archive-detail {
border: 1px solid var(--rule-strong);
border-radius: 3px;
}
.game-settings .comp button:hover {
:is(.game-settings, .motion-cards) .comp button:hover {
border-color: var(--color-muted);
}
.game-settings .comp button[data-st="on"] {
:is(.game-settings, .motion-cards) .comp button[data-st="on"] {
background: color-mix(in srgb, var(--g) 14%, transparent);
border-color: color-mix(in srgb, var(--g) 70%, transparent);
}
.game-settings .comp button[data-st="some"] {
:is(.game-settings, .motion-cards) .comp button[data-st="some"] {
border-style: dashed;
border-color: color-mix(in srgb, var(--g) 70%, transparent);
}
/* Nothing built in yet: dimmed like the mockup, but still a choice that is remembered. */
.game-settings .comp button[data-empty="true"]:not([data-st="on"]) {
:is(.game-settings, .motion-cards) .comp button[data-empty="true"]:not([data-st="on"]) {
opacity: 0.55;
}
.game-settings .comp .cbx {
:is(.game-settings, .motion-cards) .comp .cbx {
position: absolute;
top: 12px;
right: 12px;
@@ -4334,37 +4414,57 @@ dialog.archive-detail {
display: grid;
place-items: center;
}
.game-settings .comp button[data-st="on"] .cbx {
:is(.game-settings, .motion-cards) .comp button[data-st="on"] .cbx {
background: var(--g);
border-color: var(--g);
}
.game-settings .comp button[data-st="some"] .cbx::after {
:is(.game-settings, .motion-cards) .comp button[data-st="some"] .cbx::after {
content: "";
width: 8px;
height: 2px;
background: var(--g);
}
.game-settings .comp .cbx svg {
:is(.game-settings, .motion-cards) .comp .cbx svg {
width: 11px;
height: 11px;
stroke: var(--color-ground);
stroke-width: 3;
fill: none;
}
.game-settings .comp .ln {
:is(.game-settings, .motion-cards) .comp .ln {
display: block;
padding-right: 22px;
font: 600 17px/1.1 var(--font-display);
letter-spacing: 0.02em;
color: var(--color-ink);
}
.game-settings .comp .ls {
:is(.game-settings, .motion-cards) .comp .ls {
display: block;
margin-top: 3px;
font-size: 11.5px;
line-height: 1.3;
color: var(--color-faint);
}
/* Settings → Appearance → Motion & celebrations: the same cards, three across.
A card Reduce motion holds off is dimmed and cannot be pressed. */
.motion-cards {
--rule-strong: color-mix(in srgb, var(--color-ink) 22%, var(--color-surface));
}
.motion-cards .comp {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.motion-cards .comp button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.motion-cards .comp button:disabled:hover {
border-color: var(--rule-strong);
}
@media (max-width: 700px) {
.motion-cards .comp {
grid-template-columns: 1fr;
}
}
.game-settings .compnote {
display: flex;
gap: 14px;