@font-face { font-family: first; src: url(assets/fonts/first-Regular.ttf); }

/* pc: --u is a constant 1px, so every calc(N * var(--u)) is just Npx — a fixed 1920x1080 canvas
   that never grows or shrinks. A window smaller than that crops it (no scaling, no white gaps at
   the edges of the canvas itself); a bigger window shows the extra space around it. */
:root { --u: 1px; }
/* the one exception: the central paint/cyber illustration still scales with the window */
:root { --illus-u: min(100vw / 1920, 100vh / 1080); }
/* pc: artwork size shared by the world list and the world-detail page, so the same work never
   renders at two different sizes depending which page you view it from. Fixed regardless of
   window size — the detail page's text no longer needs the image to shrink to avoid overlap,
   since .info's difference blend mode handles that instead (see .info below). */
:root { --work-img-w: 650px; }
@media (max-width: 768px) {
  /* mobile keeps scaling to fit the viewport — only pc goes fixed-size */
  :root { --u: min(100vw / 360, 100vh / 800); }
}
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #000; overflow-x: hidden;
  font: calc(20.2 * var(--u)) / calc(22.5 * var(--u)) first, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; }
@media (max-width: 768px) {
  /* home only: .stage is a fixed 360x800-unit box that needs body letterboxed to the same width
     to stay centered on a viewport wider than that (e.g. a desktop window resized narrow) instead
     of sitting flush left. Other mobile pages lay out at the real viewport width — letterboxing
     them too double-counts with their own 60u padding whenever height (not width) drives --u,
     e.g. a short/wide window, ballooning the side margins well past 60. */
  body.home { max-width: calc(360 * var(--u)); margin: 0 auto; }
}
a { color: inherit; text-decoration: none; }
img { display: block; }
h1, p { margin: 0; font-weight: 400; }

/* shared chrome: logo, back arrow, arc */
/* fixed (not absolute) so they stay put on screen through scroll, on every page — world.html's
   work list is the one that actually gets tall enough to scroll under them */
.logo, .back { position: fixed; z-index: 2; }
.logo { left: calc(30 * var(--u)); top: calc(30 * var(--u)); width: 136.3px; }
/* right edge flush with the logo's own right edge (30 + 136.3 - 40), height unchanged */
.back { left: calc(126.3 * var(--u)); top: calc(167.2 * var(--u)); width: 40px; }
.logo img, .back img { width: 100%; }
/* pc: the curve at its fixed 1920x1080 size, centered — a smaller window just crops its
   left/right (and top/bottom) edges rather than shrinking or restretching the curve itself */
.arc { position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: url(assets/arc-pc.svg) 50% 50% / calc(1920 * var(--u)) calc(1080 * var(--u)) no-repeat; }
@media (max-width: 768px) {
  .logo { left: calc(50% - 40 * var(--u)); top: calc(26 * var(--u)); width: calc(80 * var(--u)); }
  .back { left: calc(60 * var(--u)); top: calc(80 * var(--u)); width: calc(28 * var(--u)); }
  /* mobile still scales to fit the viewport, so size it by percentage, not the pc-fixed px above */
  .arc { background-image: url(assets/arc-mobile.svg); background-size: 100% auto; }
}
.page { position: relative; min-height: 100vh; overflow: hidden; }

/* main */
body.home { min-height: 100vh; }
@media (max-width: 768px) {
  body.home { display: flex; justify-content: center; align-items: flex-start; }
}
/* pc: the old single flat image is now separate pieces (logo/book/illustration) so each
   can anchor to its own corner of the viewport instead of all moving together as one block.
   mobile still gets a flat logo image + hit overlay, in .stage. Each container holds one
   platform's version and is hidden outright on the other, rather than toggling piece by piece. */
.pc-main .book, .pc-main .illustration-wrap { position: fixed; }
.pc-main .book img { width: 100%; display: block; }
/* height given explicitly (not auto) — a bottom-anchored replaced element with no intrinsic size
   doesn't reliably get its height from its aspect ratio in this renderer, and stretches to fill */
.pc-main .book { left: calc(30 * var(--u)); bottom: calc(20 * var(--u)); width: 70px; height: 54.6px; }
/* fixed size like everything else on pc (not --illus-u) — a smaller window just crops it instead
   of shrinking it. right-anchored, vertically centered on the viewport's own center line */
.pc-main .illustration-wrap { right: calc(30 * var(--u)); top: 50%; transform: translateY(-50%);
  width: 275.4px; height: 174.1px; }
.pc-main .illustration-wrap img { width: 100%; height: 100%; display: block; pointer-events: none; }
.illustration-wrap .hit { left: calc(var(--x) * var(--u)); top: calc(var(--y) * var(--u));
  width: calc(var(--w) * var(--u)); height: calc(var(--h) * var(--u)); }
/* the globes themselves: an animated gif (real rendered rotation frames, not a CSS rotateY spin
   on a flat image — that squashes to a sliver at the 90°/270° point, which read as "too flat").
   cyber is mirrored to spin the opposite way from paint — the grid has no L/R-distinguishing
   marks, so a horizontal flip of the same file reads as a genuine reverse rotation, no 2nd asset. */
.pc-main .illustration-wrap .globe-paint, .pc-main .illustration-wrap .globe-cyber {
  position: absolute; left: calc(205.1 * var(--u)); width: calc(70.3 * var(--u)); height: calc(70 * var(--u)); }
.pc-main .illustration-wrap .globe-paint { top: 0; }
.pc-main .illustration-wrap .globe-cyber { top: calc(100 * var(--u)); transform: scaleX(-1); }
/* hand/arrow accent icons: pulled out of the illustration itself (an <img>'s content can't be
   hovered/targeted from outside it) so hovering the globe next to them can pull them toward it. */
/* idle: reuses the same weightless drift as the profile card (see @keyframes float below) — it
   animates translate/rotate, which compose independently of the transform below, so the hover
   pull and the drift never fight over the same property. click-through: the hit box below is
   sized to the globe alone, not this icon's own area, so clicking the icon does nothing. */
.hand-accent, .arrow-accent { position: absolute; pointer-events: none; transition: transform .6s ease-out;
  animation: float 9s ease-in-out infinite; }
/* different duration (not just a delay) so the two never settle into a fixed, mirrored offset */
.arrow-accent { animation-duration: 7s; animation-delay: -2s; }
.pc-main .illustration-wrap .hand-accent { left: calc(17.9 * var(--u)); top: calc(17.2 * var(--u)); width: calc(27.6 * var(--u)); height: auto; }
.pc-main .illustration-wrap .arrow-accent { left: 0; top: calc(156.4 * var(--u)); width: calc(31.3 * var(--u)); height: auto; }
.pc-main .hit[aria-label="PAINT"]:hover ~ .hand-accent { transform: translateX(40px); }
.pc-main .hit[aria-label="CYBER"]:hover ~ .arrow-accent { transform: translateX(40px); }

.stage { display: none; position: relative; width: calc(360 * var(--u)); height: calc(800 * var(--u)); }
.hit { position: absolute; display: block; left: calc(var(--x) * var(--u)); top: calc(var(--y) * var(--u));
  width: calc(var(--w) * var(--u)); height: calc(var(--h) * var(--u)); }
/* globes/book pulled out of the flat mobile image too, so book can stay pinned to its own
   corner (bottom-left) instead of drifting with the rest of the artwork */
.stage .globes { position: absolute; left: calc(71.3 * var(--u)); top: calc(290 * var(--u));
  width: calc(207.4 * var(--u)); height: calc(220 * var(--u)); }
.stage .globes img { width: 100%; height: 100%; display: block; pointer-events: none; }
.stage .globes .globe-paint, .stage .globes .globe-cyber {
  position: absolute; left: calc(56.9 * var(--u)); width: calc(89.3 * var(--u)); height: calc(89 * var(--u)); }
.stage .globes .globe-paint { top: 0; }
.stage .globes .globe-cyber { top: calc(131 * var(--u)); transform: scaleX(-1); }
.stage .globes .hand-accent { left: 0; top: calc(50.5 * var(--u)); width: calc(23.9 * var(--u)); height: auto; }
.stage .globes .arrow-accent { left: calc(180.2 * var(--u)); top: calc(173.7 * var(--u)); width: calc(27.1 * var(--u)); height: auto; }
.stage .hit[aria-label="PAINT"]:hover ~ .hand-accent { transform: translateX(calc(18 * var(--u))); }
.stage .hit[aria-label="CYBER"]:hover ~ .arrow-accent { transform: translateX(calc(-18 * var(--u))); }
.stage .book { position: fixed; top: auto; right: auto; left: calc(20 * var(--u)); bottom: calc(20 * var(--u)); width: calc(76 * var(--u)); height: calc(62.7 * var(--u)); }
@media (max-width: 768px) {
  .pc-main { display: none; }
  .stage { display: block; }
}

/* world: works stacked newest first, gap stays fixed while sizes vary. Height-capped so a landscape
   image keeps its own aspect instead of being stretched to a portrait image's width. */
.list { min-height: 100vh; display: flex; flex-direction: column; align-items: center;
  gap: calc(25 * var(--u)); padding: 0 0 calc(200 * var(--u)); }
/* the top padding used to just reserve clearance under the fixed logo/back button — giving the
   first work its own full-viewport slot, centered, clears that on any realistic window height too.
   pc only — mobile's own compact stacked layout below never had a top-clearance issue to begin with */
@media (min-width: 769px) {
  .list a:first-child { min-height: 100vh; display: flex; align-items: center; }
}
/* capped at 600px per the design file; no neighboring fixed panel here, so just keep it off the
   viewport edges on a narrow window */
.list img, .list video { max-width: var(--work-img-w); height: auto; }
/* iframe has no intrinsic size like img/video, so it needs an explicit width (not just max-width) —
   inside this flex column, max-width alone gives the flex algorithm nothing to size it from and it
   collapses to 0. aspect-ratio covers height; pointer-events:none passes clicks through to the
   wrapping <a> instead of the iframe eating them. */
.list iframe { width: var(--work-img-w); max-width: 100%; aspect-ratio: 4 / 3; border: 0; display: block; pointer-events: none; }
@media (max-width: 768px) {
  /* explicit 60u side padding, image fills the rest — a fixed 240u width centered by the flexbox
     only came out to a 60u gap when the container happened to be exactly 360u wide (a phone-shaped
     viewport); on anything wider (a short/wide window) the gap is whatever centering leaves over */
  .list { gap: calc(50 * var(--u)); padding: calc(150 * var(--u)) calc(60 * var(--u)) 0; align-items: stretch; }
  .list img, .list video { width: 100%; max-width: none; height: auto; }
  .list iframe { width: 100%; max-width: none; }
}

/* detail (pc): scrolling steps through the images while the page stays put */
.scroller { height: calc(100vh + (var(--n, 1) - 1) * 50vh); }
.scroller > .page { position: sticky; top: 0; height: 100vh; }
.main { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); }
/* shrinks before the window gets narrow enough for the fixed-width info panel to overlap it */
.main img, .main video { max-width: var(--work-img-w); height: auto; }
/* iframe has no intrinsic aspect ratio like img/video do, so height:auto can't size it — pick one */
.main iframe { width: var(--work-img-w); aspect-ratio: 4 / 3; border: 0; display: block; }
/* the "ghost" is whichever neighbor image scroll is currently blending toward (see render() in
   app.js) — absolutely overlaid on the settled image so both can be partially visible at once for
   a true scroll-linked crossfade. object-fit:contain since it's sized to the settled image's own
   box, not its own natural size, until scroll finishes carrying it in and it's promoted instead. */
.main .ghost { position: absolute; inset: 0; object-fit: contain; }
/* sold dot: flush with the image's own bottom edge, 20u past its right edge, so it tracks whatever size the image renders at */
.sold { position: absolute; visibility: hidden; width: calc(20 * var(--u)); height: calc(20 * var(--u));
  border: 1px solid #000; border-radius: 50%; }
.sold.paint { visibility: visible; }
.sold.on { background: #f00; border-color: #f00; }
.main .sold { left: 100%; bottom: 0; margin-left: calc(20 * var(--u)); }
/* pinned to their own viewport corner (not centre-relative) so they hold position as the window
   shrinks, and fixed-px (not --u) sized so they hold their reading size too, not just their spot */
.thumbs { position: fixed; left: calc(30 * var(--u)); bottom: calc(30 * var(--u)); width: 650px;
  display: flex; flex-wrap: wrap; gap: 17.5px; }
.thumbs .thumb { width: 52.5px; height: 40px; opacity: .35; cursor: pointer; overflow: hidden; }
.thumbs .thumb.on { opacity: 1; }
.thumbs .thumb img, .thumbs .thumb video, .thumbs .thumb iframe { width: 100%; height: 100%; object-fit: cover; display: block; border: 0; pointer-events: none; }
.slider { display: none; }
/* white text + difference blend: reads normal black on the white page, and auto-inverts pixel by
   pixel wherever it happens to sit over the artwork image instead — no overlap math needed. The
   blend-mode has to be set on .info itself (the outermost fixed-position element) to correctly see
   the true backdrop behind it — putting it on a nested descendant instead (even .info's own text
   children individually) silently stops it from blending against anything outside .info at all,
   which is also why .souvenir must NOT be a child of .info (see .souvenir below). */
/* bottom:30 flush with the page's own bottom padding — the same spot .souvenir itself sits at
   when there's no button to stack above (sold paint / cyber with no file): otherwise the panel
   would keep floating at the button-stacked height and read as detached from the bottom edge.
   when the button IS showing (next sibling .souvenir not [hidden]), stack above it instead:
   30 (base) + 20 (button height) + 20 (gap) = 70, so the button's own spot never moves. */
.info { position: fixed; right: calc(30 * var(--u)); bottom: calc(30 * var(--u)); width: 240px; font-size: 20.2px; line-height: 22.5px;
  color: #fff; mix-blend-mode: difference; }
.info:has(+ .souvenir:not([hidden])) { bottom: calc(70 * var(--u)); }
.info h1 { font-size: 27.3px; line-height: 30px; }
.info #desc { margin-top: 9.3px; white-space: pre-line; }
.info #meta { margin-top: 25px; font-size: 27.3px; line-height: 30px; }
/* pc: year and materials on their own lines; mobile (below) puts them on one line instead */
.info #meta .sep { display: none; }
.info #meta-mat { display: block; }
.info #meta .hide { display: none !important; }
/* "GET SOUVENIR?" — cyber's download / unsold-paint's buy button. Each state is a whole separate
   asset (background baked in, not just recolored text), so hover is a plain image swap — no
   filter/background-color trickery needed. Right-aligned, independent of .info (not inside it —
   see the comment above .info) but at the page's own bottom:30/right:30, same as .info's own
   default flush position, since .info moves up to make room for it when it's shown (see above). */
.souvenir { display: inline-block; position: fixed; right: calc(30 * var(--u)); bottom: calc(30 * var(--u)); }
.souvenir img { display: block; height: 20px; width: auto; }
.souvenir .on-hover { display: none; }
.souvenir:hover .on-normal { display: none; }
.souvenir:hover .on-hover { display: block; }
.souvenir[hidden] { display: none; }
@media (max-width: 768px) {
  .scroller { height: auto; }
  /* min-height: 0 cancels the shared .page rule's 100vh floor — .info now sits right after .page
     as a sibling (moved there for the pc blend-mode fix below), so that floor was pushing it a
     full screen-height down past the image slider instead of right after it */
  .scroller > .page { position: relative; height: auto; min-height: 0; }
  .main, .thumbs { display: none; }
  .slider { display: flex; align-items: flex-start; gap: calc(30 * var(--u)); margin-top: calc(150 * var(--u)); padding: 0 calc(60 * var(--u));
    overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding: 0 calc(60 * var(--u)); scrollbar-width: none; }
  .slider::-webkit-scrollbar { display: none; }
  /* padding-bottom reserves the sold dot's space below the image — an absolutely-positioned child
     doesn't grow its container on its own, and a horizontal scroller clips anything that does
     overflow it. Reserving the space keeps this correct at any image height, not just one number. */
  .slide { position: relative; flex: none; scroll-snap-align: start; padding-bottom: calc(40 * var(--u)); }
  .slider img, .slider video { width: calc(240 * var(--u)); height: auto; }
  .slider iframe { width: calc(240 * var(--u)); aspect-ratio: 4 / 3; border: 0; }
  /* the first slide always matches the world-listing image's size/position (fills the row) — same
     treatment a lone image already got, just no longer conditional on being the ONLY image. Later
     slides keep the smaller 240u size, which is what gives them their "peek of more" hint.
     width (not flex-grow) so it just overflows into the horizontal scroll area like the fixed-size
     later slides do, rather than fighting them over available space — flex-grow:1 here collapsed to
     0 width instead, since the later (flex-shrink:0) slides refuse to make room for it. */
  .slide:first-child { width: 100%; }
  .slide:first-child img, .slide:first-child video, .slide:first-child iframe { width: 100%; }
  .slide .sold { right: 0; bottom: 0; }
  /* bottom padding moved from .info onto .souvenir below (padding: ...0) — a negative margin
     pulling .souvenir up through .info's own padding to cancel it left .souvenir's box literally
     overlapping .info's (still-hit-testable) padding area, and .info — blended, so it forms its
     own stacking context — won every hover/click there instead of the button underneath it */
  .info { position: static; width: auto; padding: calc(70 * var(--u)) calc(60 * var(--u)) 0; }
  .info #meta { margin-top: calc(16.5 * var(--u)); }
  /* mobile: .info never overlaps an image (the slider sits above it, not behind), so there's no
     blend-mode reason to keep .souvenir out of the flow here — back to a normal static sibling,
     inset to match .info's own left padding since it no longer inherits that padding as a child.
     width:fit-content + margin-left:auto right-aligns it, matching .info's own right inset (60u);
     the bottom margin is .info's old trailing space, now carried by whichever element is last. */
  .souvenir { display: block; width: fit-content; position: static; left: auto; bottom: auto;
    margin: calc(20 * var(--u)) calc(60 * var(--u)) calc(60 * var(--u)) auto; }
  .souvenir img { height: calc(20 * var(--u)); width: auto; }
}

/* profile: the card drifts weightlessly around the centre */
.card { position: absolute; left: 50%; top: 50%; width: calc(619 * var(--illus-u)); transform: translate(-50%, -50%); }
.card img { width: 100%; animation: float 9s ease-in-out infinite; }
@keyframes float {
  0%, 100% { translate: 0 0; rotate: -.7deg; }
  25% { translate: calc(6 * var(--u)) calc(-9 * var(--u)); rotate: .2deg; }
  50% { translate: calc(-4 * var(--u)) calc(-14 * var(--u)); rotate: .8deg; }
  75% { translate: calc(-7 * var(--u)) calc(-4 * var(--u)); rotate: -.3deg; }
}
@media (prefers-reduced-motion: reduce) { .card img, .hand-accent, .arrow-accent { animation: none; } }
/* --illus-u (not --u) so this scales together with the card above it, which also uses --illus-u */
.welcome { position: absolute; left: 50%; top: calc(50% + 236 * var(--illus-u)); transform: translateX(-50%); text-align: center; white-space: nowrap;
  font-size: calc(46 * var(--illus-u)); line-height: calc(54 * var(--illus-u)); }
@media (max-width: 768px) {
  .card { width: calc(276 * var(--u)); }
  .welcome { top: calc(50% - 150.6 * var(--u)); font-size: calc(20.5 * var(--u)); line-height: calc(24 * var(--u)); }
}
