  :root{
    --ink:#1b1b1c;
    --paper:#ebebeb;
    --line:#dcd8cd;
    --cell-line:#474747;
    --accent:#c1502e;
    --muted:#8a8578;
    --step:.35s;
  }

  *{ box-sizing:border-box; }

  html, body{
    height:100%;
    margin:0;
    overflow:hidden;
    background:var(--paper);
  }

  body{
    display:flex;
    flex-direction:column;
    color:var(--ink);
    font-family: -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
  }

  /* ---------- stage ---------- */
  .stage{
    flex:1 1 auto;
    position:relative;
    display:flex;
    align-items:center;
    justify-content:center;
    overflow:hidden;
  }

  .hint{
    position:absolute;
    left:18px;
    bottom:16px;
    font-size:11px;
    color:var(--muted);
    letter-spacing:.02em;
    pointer-events:none;
  }

  .puzzle-grid{
    position:relative;
    display:grid;
    gap:1px;
    /* page-colored, not the dark contour color: every tile is only ever
       anchored (for CSS Grid layout) at its HOME track, then visually
       shifted elsewhere with transform while it slides. That means its
       home track briefly has nothing painted over it mid-slide — if this
       background were the dark seam color, that gap would flash dark;
       kept page-colored, it just blends in instead. */
    background:var(--paper);
    opacity:0;
    transition:opacity .9s ease;
  }

  .tile{
    position:relative;
    background:var(--paper);
    /* the dark seam itself lives on each tile (not the grid's shared
       background), so it stays a thin, correct line regardless of which
       tracks are momentarily uncovered during a slide. box-shadow is set
       per tile in JS (right/bottom edges only, skipped on the outer rim)
       so only the seams BETWEEN cells show, never a border around the
       whole grid. */
    transition-property:transform;
    transition-duration:var(--step);
    transition-timing-function:cubic-bezier(.4,0,.2,1);
    will-change:transform;
  }

  .tile-face{
    position:absolute;
    inset:0;
  }

  .tile-front{
    background-image:var(--art);
    opacity:1;
    transition:opacity .9s cubic-bezier(.22,.61,.36,1);
  }

  .tile-back{
    /* longhands, not the `background` shorthand: JS only ever sets
       background-image per tile (the project thumbnail + a legibility
       gradient) and relies on size/position/repeat staying put. */
    background-color:var(--ink);
    background-size:cover;
    background-position:center;
    background-repeat:no-repeat;
    color:var(--paper);
    display:flex;
    flex-direction:column;
    justify-content:flex-end;
    padding:10%;
    text-decoration:none;
    opacity:0;
    pointer-events:none;
    cursor:pointer;
    overflow:hidden;
    transition:opacity 1s cubic-bezier(.22,.61,.36,1);
  }
  .puzzle-grid.solved .tile:not(.no-hover):hover .tile-front{
    opacity:0;
  }
  .puzzle-grid.solved .tile:not(.no-hover):hover .tile-back{
    opacity:1;
    pointer-events:auto;
    animation:glitch-in .55s steps(1,end) 1;
  }
  .tile-back:hover{ filter:brightness(1.15); }

  @keyframes glitch-in{
    0%   { clip-path:inset(0 0 0 0); transform:translate3d(0,0,0); }
    6%   { clip-path:inset(0 0 78% 0); transform:translate3d(-5px,0,0); }
    12%  { clip-path:inset(64% 0 4% 0); transform:translate3d(4px,0,0); }
    18%  { clip-path:inset(18% 0 58% 0); transform:translate3d(-3px,0,0); }
    26%  { clip-path:inset(0 0 0 0); transform:translate3d(3px,0,0); }
    34%  { clip-path:inset(38% 0 32% 0); transform:translate3d(-2px,0,0); }
    42%  { clip-path:inset(0 0 0 0); transform:translate3d(0,0,0); }
    100% { clip-path:inset(0 0 0 0); transform:translate3d(0,0,0); }
  }
  @keyframes glitch-text{
    0%   { text-shadow:none; }
    8%   { text-shadow:-2px 0 #ff2d55, 2px 0 #21d4fd; }
    18%  { text-shadow:2px 0 #ff2d55, -2px 0 #21d4fd; }
    28%  { text-shadow:-1px 0 #ff2d55, 1px 0 #21d4fd; }
    42%  { text-shadow:none; }
    100% { text-shadow:none; }
  }

  .tile-dot{
    width:7px;
    height:7px;
    border-radius:50%;
    margin-bottom:auto;
    background:var(--dot, var(--accent));
  }
  .tile-title{
    font-size:clamp(10px, 1.6vw, 13px);
    font-weight:600;
    line-height:1.25;
  }
  .puzzle-grid.solved .tile:not(.no-hover):hover .tile-title{
    animation:glitch-text .55s steps(1,end) 1;
  }
  .tile-cta{
    font-size:clamp(8px, 1.1vw, 10px);
    color:#cfcac0;
    margin-top:4px;
    letter-spacing:.02em;
  }

  /* ---------- logo-cell "about" / "contact" links ---------- */
  .logo-link{
    position:absolute;
    border-radius:3px;
    cursor:pointer;
    transition:background-color .2s ease;
  }
  .logo-link:hover{ background:rgba(0,0,0,.07); }

  /* ---------- project zoom ("entering" a tile) ---------- */
  .zoom-overlay{
    position:fixed;
    z-index:50;
    overflow:hidden;
    display:flex;
    flex-direction:column;
    background:var(--paper);
    color:var(--ink);
    box-shadow:0 24px 70px rgba(0,0,0,.28);
    transition:
      top .6s cubic-bezier(.65,0,.35,1),
      left .6s cubic-bezier(.65,0,.35,1),
      width .6s cubic-bezier(.65,0,.35,1),
      height .6s cubic-bezier(.65,0,.35,1);
  }

  /* the slideshow fills all remaining space above the caption */
  .zoom-slideshow{
    position:relative;
    flex:1 1 auto;
    min-height:0;
    background:var(--cell-line);
  }
  .zoom-slide{
    position:absolute;
    inset:0;
    background-size:cover;
    background-position:center;
    opacity:0;
    transition:opacity 1.1s ease;
  }
  .zoom-slide.active{ opacity:1; }
  .slide-dots{
    position:absolute;
    left:50%;
    bottom:16px;
    transform:translateX(-50%);
    display:flex;
    gap:7px;
    opacity:0;
    transition:opacity .5s ease .5s;
  }
  .zoom-overlay.open .slide-dots{ opacity:1; }
  .slide-dot{
    width:6px;
    height:6px;
    padding:0;
    border:none;
    border-radius:50%;
    background:rgba(255,255,255,.5);
    cursor:pointer;
    transition:background-color .2s ease, transform .2s ease;
  }
  .slide-dot.active{
    background:#fff;
    transform:scale(1.35);
  }

  /* the small text zone below the slideshow */
  .zoom-caption{
    flex:0 0 auto;
    padding:20px 8% 26px;
    opacity:0;
    transform:translateY(10px);
    transition:opacity .5s ease .3s, transform .5s ease .3s;
  }
  .zoom-overlay.open .zoom-caption{
    opacity:1;
    transform:translateY(0);
  }
  .zoom-caption .zoom-dot{
    display:inline-block;
    width:8px;
    height:8px;
    margin-right:8px;
    border-radius:50%;
    background:var(--dot, var(--accent));
    vertical-align:middle;
  }
  .zoom-title{
    display:inline;
    font-size:clamp(17px, 2.1vw, 23px);
    font-weight:600;
    vertical-align:middle;
  }
  .zoom-cat{
    margin:4px 0 8px;
    font-size:12px;
    color:var(--muted);
  }
  .zoom-desc{
    max-width:620px;
    margin:0;
    font-size:13px;
    line-height:1.55;
    color:#55524a;
  }

  .zoom-close{
    position:absolute;
    top:22px;
    right:22px;
    z-index:2;
    width:40px;
    height:40px;
    border-radius:50%;
    border:none;
    background:rgba(255,255,255,.75);
    color:var(--ink);
    font-size:16px;
    line-height:1;
    display:flex;
    align-items:center;
    justify-content:center;
    cursor:pointer;
    opacity:0;
    transition:opacity .4s ease .5s, background-color .2s ease;
  }
  .zoom-overlay.open .zoom-close{ opacity:1; }
  .zoom-close:hover{ background:#fff; }

  .puzzle-grid.behind-zoom{
    transition:opacity .5s ease, filter .5s ease;
    opacity:.15;
    filter:blur(2px);
    pointer-events:none;
  }

  /* ---------- toast ---------- */
  .toast{
    position:fixed;
    left:50%;
    bottom:24px;
    transform:translate(-50%, 20px);
    background:var(--ink);
    color:var(--paper);
    padding:11px 16px;
    border-radius:8px;
    font-size:13px;
    opacity:0;
    pointer-events:none;
    transition:opacity .25s, transform .25s;
    max-width:80vw;
    text-align:center;
    z-index:10;
  }
  .toast.show{
    opacity:1;
    transform:translate(-50%, 0);
  }
