:root{
    color-scheme:only light;   /* T6: ダークモードだと白文字が丸見えになる */
    --bg:#ffffff;
    --noise:#c8c8c8;
    --noise-dim:#e4e4e4;
    --text:#1a1a1a;
    --count:#d0d0d0;

    --line-h:26px;      /* 断片1行の高さ */
    --slide:1100ms;     /* 定位置へ滑る時間 */
  }
  *{margin:0;padding:0;box-sizing:border-box}

  /* 高さの指定。
     iOS はアドレスバーの増減で 100vh がずれるので dvh を使う。
     対応していないブラウザ向けに 100% を先に書いておく    */
  html,body{
    height:100%;
    height:100dvh;
    background:var(--bg);
    overflow:hidden;
  }

  body{
    font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,"Noto Sans Mono CJK JP",monospace;
    font-size:13px;
    line-height:1.9;
    letter-spacing:.06em;
    position:relative;
  }

  /* ---- ノイズ層 ---------------------------------------- */
  #screen{
    position:absolute;
    top:50%; left:50%;
    transform:translate(-50%,-50%);
    white-space:pre;
    color:var(--noise);
  }
  .ch{display:inline-block}
  .dim{color:var(--noise-dim)}
  .ch::selection{background:transparent;color:var(--noise-dim)}
  .ch.cleared{color:transparent}   /* 剥がし終わった行は消す */

  /* ---- 断片層。ノイズとは完全に別 ---------------------- */
  #stack{
    position:absolute;
    top:50%; left:50%;
    transform:translate(-50%,-50%);
    width:min(92vw, 460px);
    height:calc(var(--line-h) * 10);
    pointer-events:none;
  }
  .frag{
    position:absolute;
    white-space:nowrap;
    color:var(--text);
    letter-spacing:.14em;
    opacity:0;
    transform:translate(-50%,-50%);
    /* 選択はできる。クリック判定は誤字だけに絞ってある */
    pointer-events:auto;
    -webkit-user-select:text;
    user-select:text;
    cursor:default;
    transition:
      top     var(--slide) cubic-bezier(.2,.7,.25,1),
      left    var(--slide) cubic-bezier(.2,.7,.25,1),
      opacity 420ms ease;
  }
  .frag.shown{opacity:1}
  .frag::selection{background:transparent;color:var(--text)}

  /* ---- T6 : 白の中の白 ----
     実機検証では目視できる最小値は #f4f4f4 だったが、
     それだと良い環境では普通に読めてしまう。

     Ctrl+F の検索経路は捨てた。訪問者はそこに「手紙」という語が
     あることを知らないので、原理的に検索できない。
     代わりに、上下の行間を不自然に広く取って気づかせる。
     空白の異常さが「ここに何かある」を伝える                 */
  #veil{
    position:absolute;
    left:50%;
    white-space:nowrap;
    color:#f8f8f8;
    letter-spacing:.14em;
    transform:translate(-50%,-50%);
    transition:top 900ms cubic-bezier(.2,.7,.25,1), color 900ms ease;
    pointer-events:auto;
    -webkit-user-select:text;
    user-select:text;
  }
  #veil::selection{background:#1a1a1a;color:#fff}
  #veil.found{color:var(--text)}

  /* ---- T4 : 誤字 ----
     ②だけ1文字ずつに分解してある。誤字にだけ pointer を付ける。
     探して見つけた人だけが、カーソルの変化で確信する          */
  .frag .g{display:inline-block}
  .frag .typo{
    cursor:pointer;
    pointer-events:auto;
    transition:color 2.4s ease;
  }
  .frag .typo.nudge{ color:#000 }   /* 救済。90秒で濃くなる */
  .frag .typo.fixed{ cursor:default }

  /* ---- T5/T9 : 痕跡 ----
     位置決め（translate -50%）と震えのアニメーションが
     同じ transform を奪い合って、animation が負けていた。
     外側で位置を決め、内側の ::before で見た目を持つ      */
  .mark{
    position:fixed;
    width:0; height:0;
    opacity:0;
    transition:opacity 1.4s ease;
    pointer-events:auto;
    cursor:pointer;
    z-index:5;
  }
  .mark::before{
    content:'';
    position:absolute;
    left:50%; top:50%;
    width:var(--d); height:var(--d);
    margin:calc(var(--d) / -2) 0 0 calc(var(--d) / -2);
    border-radius:50%;
    background:#bdbdbd;
    transition:background .5s ease, transform .3s ease;
  }
  /* 当たり判定を見た目より広く取る */
  .mark::after{
    content:'';
    position:absolute;
    left:50%; top:50%;
    width:40px; height:40px;
    margin:-20px 0 0 -20px;
  }

  .mark.on{opacity:1}
  .mark.hint::before{background:#8a8a8a}
  /* 順番に触れているあいだ。断片より格下なので濃くしすぎない */
  .mark.lit::before{
    background:#6e6e6e;
    transform:scale(1.3);
  }
  /* T5を終えたら沈む。消えたように見えるが存在はしている */
  .mark.spent{ pointer-events:none; cursor:default }
  .mark.spent::before{ background:#ececec; transform:scale(1) }

  /* ---- T9 : 目を覚まして震える ---- */
  @keyframes mark-shiver{
    0%,100% { transform:translateX(0) }
    25%     { transform:translateX(-1.5px) }
    75%     { transform:translateX(1.5px) }
  }
  .mark.awake{ pointer-events:auto; cursor:grab }
  .mark.awake::before{
    background:#8a8a8a;
    animation:mark-shiver 2.2s ease-in-out infinite;
  }
  .mark.held{ cursor:grabbing }
  .mark.held::before{
    background:#4a4a4a;
    animation:none;
    transform:scale(1.2);
  }
  .mark.returned{
    opacity:0;
    pointer-events:none;
    transition:opacity .8s ease;
  }

  /* 戻す先。掴んでいる間だけ見える。
     radial-gradient rgba(0,0,0,.06) では白背景でほぼ不可視だった。
     輪郭を持たせて、置く場所だと分かる形にする            */
  #nest{
    position:fixed;
    left:50%; top:50%;
    width:150px; height:150px;
    margin:-75px 0 0 -75px;
    border-radius:50%;
    border:1px dashed #c4c4c4;
    background:rgba(0,0,0,.022);
    opacity:0;
    transform:scale(.9);
    transition:opacity .35s ease, transform .35s ease;
    pointer-events:none;
    z-index:4;
  }
  #nest.on{opacity:1;transform:scale(1)}
  /* 近づくと締まる */
  #nest.near{
    border-color:#8a8a8a;
    background:rgba(0,0,0,.05);
    transform:scale(1.06);
  }

  /* ---- T8 : 発火済みの操作をもう一度やったときの反応 ----
     何も起きないが、微かに光る。
     「まだ何かある」と思わせて、組み合わせへ誘導する      */
  @keyframes spent-pulse{
    0%   { background:transparent }
    30%  { background:rgba(0,0,0,.028) }
    100% { background:transparent }
  }
  body.pulse::before{
    content:'';
    position:fixed;
    inset:0;
    pointer-events:none;
    animation:spent-pulse 900ms ease;
    z-index:1;
  }

  /* ---- T9 : 痕跡を元の位置へ戻す ----
     8個揃うと痕跡が目を覚まして震える。
     掴んで、元あった場所（画面中央）へ引き戻す。
     修復の逆。唯一「壊しにいく」トリガー                  */
  /* ---- 接続 : 第一部 → 第二部 ----
     9個揃って静止。完成の瞬間は必ず与える。
     そのあと②と⑤が同時に反応する。

     当初は⑤を横にずらしていたが、「動いた」以上の情報を持たない。
     2つ同時に反応させることで「2つで1組」という
     R1のルールをそのまま見た目で示す。

     この作品には色が1つもない（白・黒・グレーのみ）。
     だから色を足すと必ず目立つ。ただし色は記号として定着し、
     以降ずっと「色を探すゲーム」になってしまう。
     よって、教える瞬間だけ1周期借りて、あとは無彩色に戻す。

     実装の注意: .frag には位置の transition が入っており、
     applyOrder() でインラインの transition も上書きしている。
     同じ color を transition と animation で奪い合うと
     animation が鈍って何も見えなくなる（T9と同じ問題）。
     色は内側の .g に当てる                                */

  /* 1周目だけ。色が乗る */
  @keyframes pair-intro{
    0%   { color:inherit }
    45%  { color:#5f7a6a }
    100% { color:inherit }
  }
  /* 2周目以降。無彩色で薄くなる */
  @keyframes pair-breathe{
    0%,100% { color:inherit }
    50%     { color:#9a9a9a }
  }
  .frag.paired .g{
    animation:pair-intro 2600ms ease-in-out;
  }
  .frag.paired.settled .g{
    animation:pair-breathe 2600ms ease-in-out infinite;
  }

  /* 完成の合図。9個が上から順に光り、明暗が波のように流れる。
     祝う演出は作品のトーンから外れるので、
     「これで全部だ」を確認させるだけにする。
     隣同士が重なるよう、光る時間を間隔より長く取る。
     色は内側の .g に当てる（transition との競合を避ける）  */
  @keyframes frag-breathe{
    0%   { color:inherit }
    40%  { color:#a8a8a8 }
    100% { color:inherit }
  }
  .frag.breathe .g{ animation:frag-breathe 1800ms ease-in-out }

  /* 第二部に入ったら選択は要らない */
  body.part2 .frag{ -webkit-user-select:none; user-select:none; cursor:default }
  body.part2 #screen{ opacity:0; transition:opacity 2.2s ease }

  /* ---- R1 : 関係を指摘する ----
     2つの断片にまたがってなぞる。
     不正解は線が一瞬走って消える。正解は線が残る          */
  #wires{
    position:fixed;
    inset:0;
    width:100%;
    height:100%;
    pointer-events:none;
    overflow:visible;
    z-index:3;
  }
  #wires line{
    stroke:#c8c8c8;
    stroke-width:1;
    opacity:0;
  }
  #wires line.flash{ animation:wire-flash 620ms ease }
  #wires line.kept{ opacity:1; stroke:#9a9a9a; transition:opacity 1.2s ease }
  #wires line.fade{ opacity:0 }

  @keyframes wire-flash{
    0%   { opacity:0 }
    30%  { opacity:.9 }
    100% { opacity:0 }
  }

  /* なぞっている間、通った断片が薄く反応する */
  .frag.touched .g{ color:#5a5a5a; transition:color .18s ease }

  /* R1で動く断片を強調し、他を沈める（highlight方式） */
  body.dim .frag .g{ color:#c0c0c0; transition:color .5s ease }
  body.dim .frag.spotlight .g{ color:var(--text) }

  /* ---- R2 : 掴んで入れ替える ----
     R1が「関係を指摘する」なら、R2は「自分の手で動かす」。
     指摘から操作への移行                                  */
  body.r2 .frag{ cursor:grab }
  .frag.lifted{
    z-index:9;
    cursor:grabbing;
    transition:none;
  }
  .frag.lifted .g{ color:#000 }

  /* 置き場所。掴んでいる間、近づくと現れる */
  #slotline{
    position:absolute;
    left:50%;
    width:min(88vw, 420px);
    height:1px;
    margin-left:calc(min(88vw, 420px) / -2);
    background:#c4c4c4;
    opacity:0;
    transition:opacity .25s ease, top .25s ease;
    pointer-events:none;
    z-index:8;
  }
  #slotline.on{opacity:1}

  /* ---- R4 : 下から上へなぞる ----
     誘導としてカウンタが右下から右上へ移動する。
     説明せずに方向を示す                                  */
  #counter.up{
    bottom:auto;
    top:16px;
    transition:top 1.6s ease, bottom 1.6s ease, opacity 1.2s ease;
  }

  /* 救済。⑨から①へ順に薄く光る。なぞる動きを見せる */
  @keyframes hint-sweep{
    0%,100% { color:inherit }
    45%     { color:#a0a0a0 }
  }
  .frag.sweep .g{ animation:hint-sweep 1000ms ease-in-out }

  /* ---- R5 : 最後の一手 ----
     ⑨だけ周囲に第一部のノイズが残っている。
     ここだけ終わっていないことを視覚で伝える            */
  .frag .residue{
    position:absolute;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
    white-space:pre;
    color:var(--noise-dim);
    letter-spacing:.06em;
    pointer-events:none;
    opacity:0;
    transition:opacity 1.4s ease;
    z-index:-1;
  }
  .frag.haunted{ cursor:pointer }
  .frag.haunted .residue{ opacity:1 }
  .frag.cleaned .residue{ opacity:0 }

  /* 全部消えて並び直る */
  .frag.gone{ opacity:0; transition:opacity 600ms ease }
  .frag.reborn{ opacity:0 }
  .frag.reborn.in{ opacity:1; transition:opacity 900ms ease }

  /* ---- ボーナス ----
     着地後も終わらせない。
     カウンタが `9 / ? ?` になり、画面の一箇所が一度だけ反応する。
     完結の合図は出さないが、「まだある」だけは伝える。

     静止した点滅は視界に入らなかったので、動きを付ける。
     下から上へすっと現れて消える                          */
  @keyframes bonus-signal{
    0%   { opacity:0; transform:translate(-50%, 14px) }
    35%  { opacity:1; transform:translate(-50%, 0) }
    65%  { opacity:1; transform:translate(-50%, 0) }
    100% { opacity:0; transform:translate(-50%, -14px) }
  }
  #signal{
    position:fixed;
    border-radius:50%;
    background:#8a8a8a;
    width:6px; height:6px;
    transform:translate(-50%,-50%);
    opacity:0;
    pointer-events:none;
    z-index:6;
  }
  #signal.flash{ animation:bonus-signal 2800ms ease-in-out }

  /* ボーナスで現れる断片。本編の9個の右に並ぶ。
     1個目が出た時点で本編が左へずれる。
     着地の直後にずらすと余韻が壊れるので、
     見つかってから動かす                                  */
  .bonus{
    position:absolute;
    left:50%;
    white-space:nowrap;
    color:var(--text);
    letter-spacing:.14em;
    opacity:0;
    transform:translate(-50%,-50%);
    transition:opacity 2.4s ease, left 1.4s cubic-bezier(.2,.7,.25,1);
    pointer-events:none;
    z-index:7;
  }
  .bonus.in{opacity:1}

  /* 2列になったとき。本編は左、ボーナスは右。
     2列にすると幅が要るので、文字と桁を少し詰める        */
  #stack{ transition:left 1.4s cubic-bezier(.2,.7,.25,1), width 1.4s ease }
  body.twocol #stack{ left:26%; width:min(44vw, 380px) }
  body.twocol .bonus{ left:74% }
  body.twocol .frag,
  body.twocol .bonus{ font-size:12px; letter-spacing:.10em }

  /* 横幅が足りない端末は縦に積む。PCは2列。

     以前はここで html,body を height:auto にしていたが、
     中身が絶対配置だけなので高さがゼロ近くまで潰れ、
     top:50% が画面の上を指してノイズが画面外へ出た（実機で判明）。
     高さは常に確保したまま、スクロールだけを
     ボーナスが出たあと（body.twocol）に許可する          */
  @media (max-width: 640px){
    body.twocol{ overflow-y:auto }
    body.twocol #stack{ left:50%; width:min(92vw, 460px) }
    body.twocol .bonus{ left:50% }
    body.twocol .frag,
    body.twocol .bonus{ font-size:13px; letter-spacing:.14em }
  }

  /* B5: 1文字だけ書体が違う。読まないと気づかない */
  .odd-face{
    font-family:"Hiragino Mincho ProN","Yu Mincho",serif;
    cursor:pointer;
    pointer-events:auto;
  }

  /* B4: カウンタは画面にずっといて、一度も触られていない */
  #counter{ pointer-events:auto; cursor:default }
  body.part2 #counter{ cursor:pointer }

  /* B8: 余白が図形になっている。文字ではなく空白を見る */
  #gap{
    position:absolute;
    left:50%;
    transform:translateX(-50%);
    display:grid;
    grid-template-columns:repeat(7, 10px);
    gap:6px;
    opacity:0;
    transition:opacity 1.6s ease;
    pointer-events:auto;
    z-index:6;
  }
  #gap.on{opacity:1}
  #gap i{
    width:10px; height:10px;
    background:#f2f2f2;
    display:block;
  }
  #gap i.hole{ background:transparent; cursor:pointer }

  #counter{
    position:fixed;right:18px;bottom:16px;
    color:var(--count);font-size:11px;letter-spacing:.18em;
    opacity:0;transition:opacity 1.2s ease .8s;
  }
  #counter.on{opacity:1}

  #dev{
    position:fixed;left:18px;bottom:16px;
    color:#e8e8e8;font-size:10px;letter-spacing:.1em;
  }

  @media (prefers-reduced-motion:reduce){
    .frag{transition:opacity 300ms ease}
  }