/* One regular left-to-right wave starts on page load. The accelerating wave follows every 30 seconds, five times. Sound adds an independent continuous rotation to the outer tiles while it is playing. */ @media (min-width: 75rem) and (hover: hover) and (pointer: fine) { .launcher .box { --tile-x: 0s; --tile-y: 0s; --micro-delay: 0s; --flash-color: var(--palette-2); --sound-color: #0a0a0a; --sound-text-color: #ffffff; --glow-color: #ffff00; --sound-duration: 18s; } /* Small deterministic offsets keep tiles in the same row or column from starting on the exact same frame without obscuring the wave. */ .launcher .box:nth-child(4n + 1) { --micro-delay: 0.025s; } .launcher .box:nth-child(4n + 2) { --micro-delay: 0.05s; } .launcher .box:nth-child(4n + 3) { --micro-delay: 0.075s; } /* Five distinct but all very-dark, near-black tones instead of the white pass before -- the on-phase text color below switches to white to stay readable against these. Glow keeps its own separate yellow/yellow-green cycle (--glow-color below), still guaranteed different from whichever of these a tile gets. */ .launcher .box:nth-child(5n + 1) { --sound-color: #0a0a0a; } .launcher .box:nth-child(5n + 2) { --sound-color: #0d0d10; } .launcher .box:nth-child(5n + 3) { --sound-color: #100d0a; } .launcher .box:nth-child(5n + 4) { --sound-color: #0a0d0a; } .launcher .box:nth-child(5n) { --sound-color: #0d0a0d; } /* Bright yellow-to-yellow-green range, 5 shades. */ .launcher .box:nth-child(5n + 1) { --glow-color: #ffff00; } .launcher .box:nth-child(5n + 2) { --glow-color: #f2ff00; } .launcher .box:nth-child(5n + 3) { --glow-color: #e6ff00; } .launcher .box:nth-child(5n + 4) { --glow-color: #d9ff00; } .launcher .box:nth-child(5n) { --glow-color: #ccff00; } .box:is(.social, .phanpy, .abs, .config) { --flash-color: var(--palette-1); } .box:is(.code, .mealie, .calendar, .plaza, .sound, .data) { --flash-color: var(--palette-2); } .box:is(.lldap, .intra, .trek) { --flash-color: var(--palette-3); } .box.chat { --flash-color: var(--palette-4); } .box.hdm { --flash-color: var(--palette-5); } .box.guide { --flash-color: var(--palette-5); } /* X uses 0.125-second column steps; Y uses 0.5-second row steps. */ .box.code { --tile-x: 0.125s; --tile-y: 1s; } .box.phanpy { --tile-x: 0.25s; --tile-y: 1s; } .box.abs { --tile-x: 0.25s; --tile-y: 1.5s; } .box.mealie { --tile-x: 0.375s; --tile-y: 1.5s; } .box.data { --tile-x: 0.5s; --tile-y: 0s; } .box.lldap { --tile-x: 0.5s; --tile-y: 1s; } .box.trek { --tile-x: 0.5s; --tile-y: 1.5s; } .box.chat { --tile-x: 0.625s; --tile-y: 1.5s; } .box.sound { --tile-x: 0.75s; --tile-y: 0s; } .box.social { --tile-x: 0.75s; --tile-y: 1s; } .box.guide { --tile-x: 0.875s; --tile-y: 1s; } .box.calendar { --tile-x: 0.875s; --tile-y: 1.5s; } .box.intra { --tile-x: 1s; --tile-y: 1s; } .box.config { --tile-x: 1s; --tile-y: 1.5s; } .box.hdm { --tile-x: 1.125s; --tile-y: 1s; } .box.plaza { --tile-x: 1.125s; --tile-y: 1.5s; } .launcher .frame > .box:not(.hero) .tile-spin { animation: desktop-turn-y 3.333s cubic-bezier(0.16, 1, 0.3, 1) 1 forwards, desktop-color-pulse 0.75s ease-in-out 1, desktop-twenty-turns 30s linear 5 forwards, desktop-peak-color 30s linear 5; animation-delay: calc(0.5s + var(--tile-x) + var(--micro-delay)), calc(0.625s + var(--tile-x) + var(--micro-delay)), calc(30s + var(--tile-x) + var(--micro-delay)), calc(30s + var(--tile-x) + var(--micro-delay)); } .frame.sound-active > .box:not(.hero, .photo) { will-change: transform; animation: sound-intro 15s cubic-bezier(0.65, 0, 0.35, 1) 1 forwards, sound-spin var(--sound-duration) linear infinite; animation-delay: calc(var(--tile-x) + var(--micro-delay)), calc(15s + var(--tile-x) + var(--micro-delay)); } /* Sound mode replaces the scheduled inner animations. Same duration and delay as sound-spin above (not its own offset) -- that's what keeps this in lockstep with sound-spin's 90-degree holds, so color is only ever on while a tile is actually turning. */ .frame.sound-active > .box:not(.hero, .photo) .tile-spin { animation: sound-color-wave var(--sound-duration) linear infinite; animation-delay: calc(15s + var(--tile-x) + var(--micro-delay)); } /* These two tiles are coloured at rest in the regular layout. During sound mode they join the neutral tiles between colour waves. */ .frame.sound-active > :is(.guide, .sound) .tile-spin { --tile-bg: #b8b8b8; background-color: var(--tile-bg); } /* Photos rest at their natural crop. Sound mode starts one long, continuous push-in and releases it more quickly when switched off. */ .launcher .photo img { --photo-scale: 10; --photo-duration: 135s; --photo-origin: 50% 50%; transform-origin: var(--photo-origin); transition: transform 4s cubic-bezier(0.16, 1, 0.3, 1); } .launcher .photo-1 img { --photo-origin: 48% 52%; } .launcher .photo-2 img { --photo-scale: 8; --photo-duration: 160s; --photo-origin: 65% 40%; } .launcher .photo-3 img { --photo-scale: 12; --photo-duration: 180s; --photo-origin: 35% 60%; } .launcher .photo-4 img { --photo-scale: 9; --photo-duration: 125s; --photo-origin: 58% 48%; } .launcher .frame.sound-active > .photo img { transform: scale(var(--photo-scale)); transition-duration: var(--photo-duration); transition-timing-function: cubic-bezier(0.65, 0, 0.35, 1); will-change: transform; } .frame.sound-active > .box:nth-child(4n + 1) { --sound-duration: 16.5s; } .frame.sound-active > .box:nth-child(4n + 2) { --sound-duration: 19.5s; } .frame.sound-active > .box:nth-child(4n + 3) { --sound-duration: 21s; } .frame.sound-active > .box:nth-child(5n) { --sound-duration: 17.25s; } } /* The iMac layout has different coordinates. */ @media (min-width: 140rem) and (hover: hover) and (pointer: fine) { .box.hdm { --tile-x: 0.125s; --tile-y: 0.5s; } .box.config { --tile-x: 0.25s; --tile-y: 0.5s; } .box.plaza { --tile-x: 0.25s; --tile-y: 1.5s; } .box.social { --tile-x: 0.375s; --tile-y: 0.5s; } .box.guide { --tile-x: 0.375s; --tile-y: 2s; } .box.phanpy { --tile-x: 0.5s; --tile-y: 2s; } .box.data { --tile-x: 0.625s; --tile-y: 0.5s; } .box.sound { --tile-x: 0.625s; --tile-y: 3s; } .box.lldap { --tile-x: 1.125s; --tile-y: 3s; } .box.calendar { --tile-x: 1.25s; --tile-y: 0.5s; } .box.code { --tile-x: 1.25s; --tile-y: 1s; } .box.intra { --tile-x: 1.25s; --tile-y: 3s; } .box.mealie { --tile-x: 1.375s; --tile-y: 0.5s; } .box.chat { --tile-x: 1.5s; --tile-y: 1s; } .box.abs { --tile-x: 1.5s; --tile-y: 1.5s; } .box.trek { --tile-x: 1.625s; --tile-y: 1s; } .box.filler-8 { --tile-x: 0s; --tile-y: 0.5s; } .box.filler-2 { --tile-x: 0.125s; --tile-y: 1s; } .box.filler-6 { --tile-x: 0.25s; --tile-y: 2s; } .box.filler-7 { --tile-x: 0.375s; --tile-y: 2.5s; } .box.filler-11 { --tile-x: 1.5s; --tile-y: 3s; } .box.filler-3 { --tile-x: 1.625s; --tile-y: 0.5s; } .box.filler-4 { --tile-x: 1.75s; --tile-y: 0.5s; } .box.filler-5 { --tile-x: 1.75s; --tile-y: 1s; } .box.filler-9 { --tile-x: 1.875s; --tile-y: 0.5s; } .imac-filler { --flash-color: var(--palette-3); } } @keyframes desktop-turn-y { 0% { transform: translateZ(0) rotateY(0deg); } 100% { transform: translateZ(0) rotateY(360deg); } } @keyframes desktop-color-pulse { 0%, 100% { background-color: var(--tile-bg); } 35%, 65% { background-color: var(--flash-color); } } @keyframes desktop-twenty-turns { 0% { transform: translateZ(0) rotateY(0deg); animation-timing-function: ease-in; } 14% { transform: translateZ(0) rotateY(1800deg); animation-timing-function: linear; } 26% { transform: translateZ(0) rotateY(5400deg); animation-timing-function: ease-out; } 40%, 100% { transform: translateZ(0) rotateY(7200deg); } } @keyframes desktop-peak-color { 0%, 12%, 28%, 100% { background-color: var(--tile-bg); } 14%, 26% { background-color: var(--flash-color); } } /* Rises from true flat rest (0turn, the tile's actual idle pose before sound has ever been switched on) up to the 90-degree hold position and then just stays there -- hands off directly into sound-spin below, which starts its loop already sitting at that same 0.25turn angle. */ @keyframes sound-intro { 0% { transform: perspective(40rem) rotateY(0turn); } 35% { transform: perspective(40rem) rotateY(0.25turn); } 100% { transform: perspective(40rem) rotateY(0.25turn); } } /* Every hold sits at a 90-degree-equivalent angle (0.25turn, then 1.25turn -- rotateY is periodic mod 1turn so both read as the same sideways pose), and the loop itself starts and ends on a hold (0% and 100% are both 0.25turn-equivalent) so color and position stay in lockstep with sound-color-wave below: whenever color is off, rotation is exactly at rest on one of these two stops, never mid-turn. Between holds it spins at an uneven pace -- a quick burst then a slower stretch -- to the next stop. */ @keyframes sound-spin { 0% { transform: perspective(40rem) rotateY(0.25turn); } 22% { transform: perspective(40rem) rotateY(0.25turn); } 30% { transform: perspective(40rem) rotateY(0.6turn); } 48% { transform: perspective(40rem) rotateY(1.25turn); } 70% { transform: perspective(40rem) rotateY(1.25turn); } 78% { transform: perspective(40rem) rotateY(1.65turn); } 100% { transform: perspective(40rem) rotateY(2.25turn); } } /* Rest points (0%, 22%, 48%, 70%, 100%) land exactly on sound-spin's 90-degree holds above -- color is only ever on while the tile is actually mid-turn, off precisely when it's sitting still. */ @keyframes sound-color-wave { 0%, 22%, 48%, 70%, 100% { background-color: transparent; box-shadow: none; color: inherit; } 30%, 40%, 80%, 92% { background-color: var(--sound-color); box-shadow: 0 0 0.08rem 0 var(--glow-color), 0 0 0.16rem 0.01rem var(--glow-color); color: var(--sound-text-color); } } @media (prefers-color-scheme: dark) { .frame.sound-active > :is(.guide, .sound) .tile-spin { --tile-bg: #3a3a3a; } /* Dark mode: a dark-gray-to-black range (light mode gets a single standard white instead, see below) -- text stays white for contrast against these. */ .launcher .box:nth-child(5n + 1) { --sound-color: #1a1a1a; } .launcher .box:nth-child(5n + 2) { --sound-color: #141414; } .launcher .box:nth-child(5n + 3) { --sound-color: #0d0d0d; } .launcher .box:nth-child(5n + 4) { --sound-color: #070707; } .launcher .box:nth-child(5n) { --sound-color: #000000; } .launcher .box:nth-child(5n + 1), .launcher .box:nth-child(5n + 2), .launcher .box:nth-child(5n + 3), .launcher .box:nth-child(5n + 4), .launcher .box:nth-child(5n) { --sound-text-color: #ffffff; } } /* Light mode: cold pale-yellow variation instead of dark mode's dark-gray-to-black range above, and skip the glow entirely. */ @media (prefers-color-scheme: light) { /* Cold (slightly green-leaning, not orange-warm) pale yellow range, 5 shades. */ .launcher .box:nth-child(5n + 1) { --sound-color: #f7f7a8; } .launcher .box:nth-child(5n + 2) { --sound-color: #f0f090; } .launcher .box:nth-child(5n + 3) { --sound-color: #eaea80; } .launcher .box:nth-child(5n + 4) { --sound-color: #e3e370; } .launcher .box:nth-child(5n) { --sound-color: #dcdc60; } .launcher .box:nth-child(5n + 1), .launcher .box:nth-child(5n + 2), .launcher .box:nth-child(5n + 3), .launcher .box:nth-child(5n + 4), .launcher .box:nth-child(5n) { --sound-text-color: #04210f; /* No glow in light mode -- transparent makes the box-shadow's color stops invisible without needing to restructure the shadow itself away from var(--glow-color). */ --glow-color: transparent; } } @media (prefers-reduced-motion: reduce) { .launcher .frame > .box .tile-spin, .frame.sound-active > .box { animation: none; box-shadow: var(--tile-shadow); } .launcher .frame.sound-active > .photo img { transform: scale(1); transition: none; } }