Files
Portfolio/src/styles/world.css
LucaP 9b79e031ff fix: let the mouse wheel scroll the reading viewport
overscroll-behavior: none on the script-scrolled, overflow-hidden reading
viewport swallowed wheel events, so the page only moved by keyboard,
touch or the navigation.
2026-10-10 23:30:58 +03:00

627 lines
15 KiB
CSS

.visually-hidden {
position: absolute !important;
inline-size: 1px !important;
block-size: 1px !important;
margin: -1px !important;
overflow: hidden !important;
clip-path: inset(50%) !important;
white-space: nowrap !important;
}
.journey-world {
position: fixed;
z-index: 0;
inset: 0;
overflow: hidden;
pointer-events: none;
background: #040713;
}
.journey-poster,
.journey-canvas {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
}
/* A still destination grade when motion is disabled; no animation or WebGL. */
.journey-world[data-static-fallback='true'][data-arrived='true'] .journey-poster {
filter: sepia(0.35) saturate(0.85);
}
.journey-poster {
object-fit: cover;
background:
radial-gradient(ellipse at 50% 47%, rgb(242 31 139 / 34%) 0 7%, rgb(110 21 113 / 25%) 20%, transparent 48%),
linear-gradient(180deg, #01030a 0 25%, #070817 43%, #1b0b27 59%, #02060b 100%);
background-size: cover;
}
.journey-poster-composition,
.journey-poster-composition > span {
position: absolute;
}
.journey-poster-composition {
inset: 0;
overflow: hidden;
}
.journey-poster__stars {
inset: 0;
opacity: 0.72;
background-image:
radial-gradient(circle, rgb(225 252 255 / 90%) 0 1px, transparent 1.3px),
radial-gradient(circle, rgb(120 216 229 / 72%) 0 0.8px, transparent 1.1px);
background-position: 17px 29px, 83px 97px;
background-size: 173px 149px, 227px 181px;
mask-image: linear-gradient(180deg, #000 0 54%, transparent 72%);
}
.journey-poster__sun {
inset: 34% auto auto 50%;
inline-size: clamp(8rem, 13.5vw, 14.5rem);
aspect-ratio: 1;
translate: -50% 0;
border-radius: 50%;
background:
repeating-linear-gradient(
180deg,
transparent 0 8%,
rgb(51 5 31 / 72%) 8.5% 11%,
transparent 11.5% 15%
),
linear-gradient(180deg, #ff4d9b, #d52378 58%, #8e155c);
box-shadow:
0 0 2.4rem rgb(255 44 151 / 42%),
0 1.2rem 8rem 2.4rem rgb(126 29 143 / 28%);
}
.journey-poster__terrain {
inset: 45% -12% -8%;
background:
repeating-linear-gradient(0deg, transparent 0 7.6%, rgb(84 221 233 / 19%) 7.8% 8.15%, transparent 8.35% 12%),
repeating-linear-gradient(76deg, transparent 0 6.8%, rgb(220 50 168 / 17%) 7% 7.25%, transparent 7.5% 11%),
repeating-linear-gradient(104deg, transparent 0 8.4%, rgb(73 207 228 / 14%) 8.6% 8.85%, transparent 9.1% 13%),
linear-gradient(180deg, rgb(104 21 101 / 34%), rgb(3 10 17 / 86%));
clip-path: polygon(0 17%, 6% 8%, 12% 20%, 18% 13%, 24% 25%, 30% 17%, 36% 29%, 42% 23%, 47% 34%, 50% 41%, 53% 34%, 58% 23%, 64% 29%, 70% 17%, 76% 25%, 82% 13%, 88% 20%, 94% 8%, 100% 17%, 100% 100%, 0 100%);
filter: drop-shadow(0 -0.35rem 1.5rem rgb(223 40 169 / 22%));
}
.journey-poster__road {
inset: 46% -8% -10%;
background:
repeating-linear-gradient(180deg, transparent 0 7%, rgb(244 177 80 / 84%) 7.35% 8.35%, transparent 8.7% 15%) 50% 0 / clamp(0.15rem, 0.32vw, 0.34rem) 100% no-repeat,
linear-gradient(90deg, rgb(46 188 212 / 15%), transparent 5% 95%, rgb(46 188 212 / 15%)),
linear-gradient(180deg, #050813, #000207);
clip-path: polygon(49.55% 0, 50.45% 0, 76% 100%, 24% 100%);
}
.journey-poster__rail {
inset: 0;
background: #67d4e4;
filter: drop-shadow(0 0 0.55rem rgb(76 207 225 / 72%));
}
.journey-poster__rail--left {
clip-path: polygon(49.38% 46%, 49.5% 46%, 24.3% 100%, 23.55% 100%);
}
.journey-poster__rail--right {
clip-path: polygon(50.5% 46%, 50.62% 46%, 76.45% 100%, 75.7% 100%);
}
.journey-poster__car {
inset: auto auto 1.6% 50%;
inline-size: clamp(9.5rem, 20vw, 21rem);
block-size: clamp(3.6rem, 7.4vw, 7.8rem);
translate: -50% 0;
border-radius: 40% 40% 8% 8% / 34% 34% 12% 12%;
background:
linear-gradient(106deg, transparent 0 24%, rgb(13 41 56 / 98%) 24.5% 42%, rgb(3 13 22 / 98%) 42.5% 57.5%, rgb(13 41 56 / 98%) 58% 75.5%, transparent 76%) 50% 6% / 78% 48% no-repeat,
linear-gradient(180deg, #173444 0 26%, #0a2231 46%, #06131f 68%, #020711 100%);
clip-path: polygon(29% 0, 71% 0, 82% 25%, 95% 43%, 100% 76%, 94% 100%, 6% 100%, 0 76%, 5% 43%, 18% 25%);
filter:
drop-shadow(0 0.8rem 1.2rem rgb(0 0 0 / 82%))
drop-shadow(0 0.2rem 1.1rem rgb(46 181 207 / 12%));
}
.journey-poster__car::before {
position: absolute;
inset: 61% 8% auto;
block-size: clamp(0.24rem, 0.38vw, 0.42rem);
border-radius: 50%;
background: linear-gradient(90deg, #ff244f, #ff7592 48% 52%, #ff244f);
box-shadow: 0 0 1rem rgb(255 49 95 / 64%);
content: '';
}
.journey-poster__car::after {
position: absolute;
inset: 88% 20% auto;
block-size: 0.16rem;
border-radius: 50%;
background: #67d4e4;
box-shadow: 0 0 1.1rem 0.36rem rgb(61 208 229 / 48%);
content: '';
}
.journey-canvas {
max-inline-size: none;
}
.portfolio-shell {
position: relative;
z-index: 1;
}
.site-header {
position: relative;
z-index: 2;
}
.portfolio-main {
isolation: isolate;
}
.journey-hud {
position: fixed;
z-index: var(--z-hud);
inset: auto max(0.75rem, env(safe-area-inset-right)) max(0.75rem, env(safe-area-inset-bottom)) auto;
display: flex;
min-block-size: 44px;
align-items: center;
gap: 0.72rem;
padding: 0.48rem 0.72rem;
border: 1px solid rgb(126 220 232 / 25%);
border-radius: 2px;
background: var(--surface-strong);
box-shadow: 0 0.8rem 2.4rem rgb(0 5 14 / 28%);
color: var(--text-muted);
font: 560 0.72rem/1 var(--mono);
letter-spacing: 0.08em;
pointer-events: none;
}
.journey-hud__route {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
}
.journey-hud__route span {
color: rgb(170 185 194 / 70%);
font-size: 0.61rem;
}
.journey-hud__route strong {
color: var(--text);
font-size: 0.76rem;
}
.journey-hud__section,
.journey-hud__arrival {
color: var(--cyan);
}
.journey-hud__section::before {
margin-inline-end: 0.72rem;
color: rgb(126 220 232 / 35%);
content: '/';
}
.journey-hud__arrival {
padding-inline-start: 0.12rem;
color: var(--amber);
}
.drive-controls {
position: fixed;
z-index: var(--z-popover);
inset: auto auto max(0.75rem, env(safe-area-inset-bottom)) max(0.75rem, env(safe-area-inset-left));
display: flex;
align-items: center;
gap: 0.38rem;
max-inline-size: calc(100vw - 1.5rem);
font-family: var(--mono);
font-size: 0.72rem;
}
.drive-controls__dock {
display: flex;
gap: 1px;
padding: 3px;
border: 1px solid rgb(126 220 232 / 24%);
border-radius: 3px;
background: var(--surface-strong);
box-shadow: 0 0.8rem 2.4rem rgb(0 5 14 / 28%);
}
.drive-start,
.drive-control-button {
min-block-size: 44px;
border: 1px solid transparent;
border-radius: 2px;
font-weight: 620;
cursor: pointer;
transition: opacity var(--transition), transform var(--transition);
}
.drive-start {
display: inline-flex;
min-inline-size: 12.5rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding-inline: 1rem;
border-color: rgb(120 216 229 / 72%);
background: rgb(120 216 229 / 94%);
box-shadow: 0 0 1.5rem rgb(77 201 220 / 17%);
color: #041017;
letter-spacing: 0.1em;
}
.drive-start:hover:not(:disabled) {
transform: translateY(-2px);
}
.drive-start:disabled {
cursor: wait;
opacity: 0.64;
}
.drive-control-button {
position: relative;
display: inline-grid;
inline-size: 44px;
place-items: center;
padding: 0;
background: rgb(8 17 29 / 92%);
color: var(--text-muted);
}
.drive-control-button:hover {
color: var(--cyan);
transform: translateY(-1px);
}
.drive-controls__settings-trigger {
border-color: rgb(126 220 232 / 24%);
background: var(--surface-strong);
box-shadow: 0 0.8rem 2.4rem rgb(0 5 14 / 28%);
}
.drive-start:focus-visible,
.drive-start:focus,
.drive-control-button:focus-visible,
.drive-control-button:focus,
.drive-popover input:focus-visible,
.drive-popover select:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px var(--focus), 0 0 0 1px rgb(3 8 17 / 90%);
}
.drive-control-button::after {
position: absolute;
inset: auto auto calc(100% + 0.5rem) 50%;
padding: 0.32rem 0.46rem;
transform: translate(-50%, 4px);
border: 1px solid rgb(126 220 232 / 18%);
background: rgb(3 8 17 / 96%);
color: var(--text);
content: attr(data-tooltip);
font: 520 0.65rem/1 var(--mono);
letter-spacing: 0.02em;
opacity: 0;
pointer-events: none;
transition: opacity var(--transition), transform var(--transition);
white-space: nowrap;
}
.drive-control-button:hover::after,
.drive-control-button:focus-visible::after {
transform: translate(-50%, 0);
opacity: 1;
}
.drive-popover {
position: absolute;
inset: auto auto calc(100% + 0.7rem) 0;
display: grid;
transform-origin: left bottom;
border: 1px solid rgb(126 220 232 / 27%);
border-radius: 3px;
background: var(--surface-strong);
box-shadow: 0 1.4rem 3rem rgb(0 4 12 / 42%);
color: var(--text);
animation: drive-popover-in var(--transition) both;
}
@keyframes drive-popover-in {
from {
transform: translateY(6px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.drive-popover--volume {
grid-template-columns: auto minmax(8rem, 1fr) auto;
min-inline-size: 16.5rem;
align-items: center;
gap: 0.7rem;
padding: 0.7rem;
}
.drive-popover--settings {
grid-template-columns: auto minmax(7rem, 1fr);
min-inline-size: 15rem;
gap: 0.55rem 1rem;
padding: 0.85rem;
}
.drive-popover strong,
.drive-popover label {
color: var(--text-muted);
font-size: 0.64rem;
font-weight: 560;
letter-spacing: 0.1em;
}
.drive-popover strong {
grid-column: 1 / -1;
padding-block-end: 0.45rem;
border-block-end: 1px solid rgb(126 220 232 / 14%);
color: var(--cyan);
}
.drive-popover input,
.drive-popover select {
min-block-size: 44px;
}
.drive-popover input[type="range"] {
inline-size: 100%;
accent-color: var(--cyan);
}
.drive-popover select {
border: 1px solid rgb(126 220 232 / 24%);
border-radius: 2px;
background: #08111d;
color: var(--text);
padding-inline: 0.65rem;
}
.drive-popover output {
color: var(--cyan);
font-size: 0.68rem;
}
.drive-controls__audio-status {
display: inline-flex;
min-block-size: 44px;
align-items: center;
margin: 0;
padding-inline: 0.85rem;
border: 1px solid rgb(244 173 103 / 26%);
border-radius: 2px;
background: var(--surface-strong);
color: var(--text-muted);
}
.drive-controls__status {
grid-column: 1 / -1;
margin: 0;
padding-block-start: 0.4rem;
border-block-start: 1px solid rgb(126 220 232 / 14%);
color: var(--text-muted);
font-size: 0.66rem;
}
@media (max-width: 767px) {
html {
scroll-padding-block-end: 7rem;
}
#contact {
padding-block-end: 7rem;
}
#contact a {
scroll-margin-block-end: 7rem;
}
.drive-controls {
inset-inline-start: max(0.5rem, env(safe-area-inset-left));
inset-block-end: max(0.5rem, env(safe-area-inset-bottom));
}
.journey-hud {
inset-inline-end: max(0.5rem, env(safe-area-inset-right));
inset-block-end: max(0.5rem, env(safe-area-inset-bottom));
max-inline-size: 6.5rem;
gap: 0.3rem;
padding-inline: 0.55rem;
letter-spacing: 0.04em;
}
.journey-hud__route span,
.journey-hud__section::before {
display: none;
}
.journey-hud__section {
max-inline-size: 4.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.journey-hud__arrival {
display: none;
}
.drive-start {
min-inline-size: 9rem;
gap: 0.6rem;
padding-inline: 0.62rem;
font-size: 0.6rem;
}
.drive-popover {
position: fixed;
inset: auto 0.5rem calc(max(0.5rem, env(safe-area-inset-bottom)) + 3.25rem);
max-inline-size: calc(100vw - 1rem);
}
}
@media (max-height: 420px) {
.journey-hud {
display: none;
}
.drive-controls {
position: relative;
inset: auto;
inline-size: fit-content;
max-inline-size: calc(100% - 1.5rem);
margin: 0.75rem;
}
#contact {
padding-block-end: var(--space-3);
}
.drive-popover {
position: absolute;
inset: auto auto calc(100% + 0.5rem) 0;
}
}
/* Round 5 outro: letterbox bars close in on arrival, on every tier (they are
CSS, so the static poster gets them too). They sit in the world layer,
under the reading column, so they frame the sky and never cover content.
The upper bar hangs below the gantry and carries a closing credit built
from real identity facts; the lower bar is left clear for the trip
summary that ends the contact chapter. */
.journey-world::before,
.journey-world::after {
position: absolute;
z-index: 1;
inset-inline: 0;
block-size: max(54px, 10vh);
background: #020108;
content: '';
transform: scaleY(0);
transition: transform 1.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.journey-world::before {
inset-block-start: 0;
display: grid;
align-items: end;
justify-items: center;
block-size: calc(var(--gantry-h, 72px) + max(30px, 4.5vh));
padding: 0 16px max(8px, 1.2vh);
color: rgb(255 226 196 / 0%);
font: 600 0.6rem/1.3 var(--mono, ui-monospace, monospace);
letter-spacing: 0.32em;
text-align: center;
text-transform: uppercase;
white-space: nowrap;
content: attr(data-credit) / '';
transform-origin: top;
transition:
transform 1.6s cubic-bezier(0.65, 0, 0.35, 1),
color 1.2s ease;
}
.journey-world::after {
inset-block-end: 0;
transform-origin: bottom;
}
.journey-world[data-arrived='true']::before,
.journey-world[data-arrived='true']::after {
transform: scaleY(1);
transition-delay: 0.5s;
}
.journey-world[data-arrived='true']::before {
color: rgb(255 226 196 / 74%);
transition-delay: 0.5s, 1.9s;
}
@media (max-width: 767px) {
.journey-world::after { block-size: max(40px, 7vh); }
.journey-world::before {
font-size: 0.5rem;
letter-spacing: 0.14em;
white-space: normal;
}
}
@media (prefers-reduced-motion: reduce) {
.journey-world::before,
.journey-world::after,
.journey-world[data-arrived='true']::before,
.journey-world[data-arrived='true']::after {
transition: none;
}
}
@media (forced-colors: active) {
.journey-world::before,
.journey-world::after { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.journey-poster {
filter: saturate(0.86) brightness(0.92);
}
}
@media print {
.journey-world,
.journey-hud,
.drive-controls,
#journey-scene-description {
display: none !important;
}
}
/* The outer stage retains the native document height. The viewport owns one
reusable alpha mask; only its content translates when the document scrolls. */
.reading-stage[data-reading-stage='live'] > .reading-viewport {
position: sticky;
top: 0;
/* A real scroll container (scrolled from script, never by the user), so
sticky children and view timelines resolve against the reading window. */
overflow: hidden;
/* Wheel and touch scrolling must chain through to the document, which
drives this container: no overscroll-behavior here. */
isolation: isolate;
}
/* The stationary reading edge supplies the live reveal. View timelines use
layout positions, which no longer follow the translated content here. */
.reading-stage[data-reading-stage='live'] :is(.portfolio-section, .project-stage__lead) > h2,
.reading-stage[data-reading-stage='live'] .project-case-study__header {
animation: none;
}
@media print {
.reading-stage,
.reading-viewport {
height: auto !important;
}
.reading-stage[data-reading-stage='live'] > .reading-viewport {
position: static;
overflow: visible;
mask-image: none !important;
}
.reading-stage .portfolio-main {
transform: none !important;
will-change: auto;
}
}