Spaces:
Sleeping
Sleeping
Update app.py, methods.html, webapp/index.html, webapp/mockServiceWorker.js, webapp/assets/index-LC1qePO9.js, webapp/assets/index-w435VGSR.css
Browse files- app.py +15 -1
- methods.html +0 -0
- webapp/assets/index-LC1qePO9.js +0 -0
- webapp/assets/index-w435VGSR.css +1 -0
- webapp/index.html +19 -0
- webapp/mockServiceWorker.js +361 -0
app.py
CHANGED
|
@@ -31,7 +31,7 @@ import threading
|
|
| 31 |
import time
|
| 32 |
|
| 33 |
import numpy as np
|
| 34 |
-
from flask import Flask, request, jsonify, send_file, abort
|
| 35 |
|
| 36 |
import morph
|
| 37 |
import probe
|
|
@@ -328,8 +328,22 @@ def _classical_mds(D: np.ndarray, dim: int = 2) -> np.ndarray:
|
|
| 328 |
# Pages
|
| 329 |
# --------------------------------------------------------------------------- #
|
| 330 |
|
|
|
|
|
|
|
|
|
|
| 331 |
@app.get("/")
|
| 332 |
def index():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
return send_file("copilot.html")
|
| 334 |
|
| 335 |
|
|
|
|
| 31 |
import time
|
| 32 |
|
| 33 |
import numpy as np
|
| 34 |
+
from flask import Flask, request, jsonify, send_file, send_from_directory, abort
|
| 35 |
|
| 36 |
import morph
|
| 37 |
import probe
|
|
|
|
| 328 |
# Pages
|
| 329 |
# --------------------------------------------------------------------------- #
|
| 330 |
|
| 331 |
+
WEBAPP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "webapp")
|
| 332 |
+
|
| 333 |
+
|
| 334 |
@app.get("/")
|
| 335 |
def index():
|
| 336 |
+
spa = os.path.join(WEBAPP, "index.html")
|
| 337 |
+
return send_file(spa) if os.path.exists(spa) else send_file("copilot.html")
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
@app.get("/assets/<path:asset>")
|
| 341 |
+
def spa_assets(asset):
|
| 342 |
+
return send_from_directory(os.path.join(WEBAPP, "assets"), asset)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
@app.get("/classic")
|
| 346 |
+
def classic():
|
| 347 |
return send_file("copilot.html")
|
| 348 |
|
| 349 |
|
methods.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
webapp/assets/index-LC1qePO9.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
webapp/assets/index-w435VGSR.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";.board{min-width:0}.board__toolbar,.board__toolbar-actions,.board__remaining,.board__game-over,.board__legend-popover span{align-items:center;display:flex}.board__toolbar{justify-content:space-between;gap:var(--space-6);margin-bottom:var(--space-4)}.board__toolbar h2{margin:0;font-size:clamp(1rem,1.5vw,1.25rem)}.board__eyebrow{margin:0 0 var(--space-1);color:var(--cn-text-dim);font-size:.68rem}.board__toolbar-actions{gap:var(--space-3)}.board__remaining{gap:var(--space-4);padding-inline:var(--space-3);color:var(--cn-text-dim);font-size:.7rem}.board__remaining span{align-items:center;gap:var(--space-1);display:inline-flex}.board__remaining .role-red{color:var(--cn-red-text)}.board__remaining .role-blue{color:var(--cn-blue-text)}.board__legend{position:relative}.board__legend-popover{z-index:20;top:calc(100% + var(--space-2));gap:var(--space-3);min-width:9rem;padding:var(--space-4);border:1px solid var(--color-divider);border-radius:var(--radius-md);background:var(--cn-sheet);box-shadow:var(--shadow-md);font-size:.78rem;display:grid;position:absolute;inset-inline-end:0}.board__legend-popover span{gap:var(--space-3)}.board__game-over{justify-content:center;gap:var(--space-3);margin-bottom:var(--space-4);min-height:2.5rem;color:var(--cn-text);border:1px solid var(--cn-assassin-dot);border-radius:var(--radius-md);background:#07070a;font-weight:700}.board__grid{border-radius:var(--radius-lg);background:var(--cn-board-well);direction:rtl;border:1px solid #ffffff06;grid-template-columns:repeat(5,minmax(0,1fr));gap:clamp(5px,.65vw,9px);padding:clamp(8px,1vw,14px);display:grid;box-shadow:inset 0 8px 24px #00000047}.board-cell{min-width:0;position:relative}.board-tile{--tile-ring:var(--cn-neutral-ring);--tile-tint:var(--cn-neutral-tint);--tile-gradient:var(--cn-neutral-grad);aspect-ratio:1.5;border-radius:var(--radius-md);background:var(--cn-card-face);width:100%;min-width:0;color:var(--cn-card-ink);box-shadow:inset 0 0 0 2px var(--tile-ring), inset 0 0 22px var(--tile-tint), inset 0 1.5px 0 #ffffff8c, inset 0 -3px 6px #785f3747, 0 3px 7px #00000080;cursor:pointer;border:0;padding:0;transition:transform .14s,box-shadow .14s;position:relative;overflow:hidden}.board-tile.role-red{--tile-ring:var(--cn-red-ring);--tile-tint:var(--cn-red-tint);--tile-gradient:var(--cn-red-grad);--tile-role-icon:var(--cn-red-text);background:var(--cn-red-grad)}.board-tile.role-blue{--tile-ring:var(--cn-blue-ring);--tile-tint:var(--cn-blue-tint);--tile-gradient:var(--cn-blue-grad);--tile-role-icon:var(--cn-blue-text);background:var(--cn-blue-grad)}.board-tile.role-neutral{--tile-ring:var(--cn-neutral-ring);--tile-tint:var(--cn-neutral-tint);--tile-gradient:var(--cn-neutral-grad);--tile-role-icon:var(--cn-neutral-icon)}.board-tile.role-assassin{--tile-ring:var(--cn-assassin-ring);--tile-tint:var(--cn-assassin-tint);--tile-gradient:var(--cn-assassin-grad);--tile-role-icon:var(--cn-assassin-dot);background:var(--cn-assassin-grad)}.board-cell:hover .board-tile:not(.is-chosen){transform:translateY(-1px)}.board-tile.is-selected{box-shadow:inset 0 0 0 2px var(--tile-ring), inset 0 0 22px var(--tile-tint), inset 0 1.5px 0 #ffffff8c, 0 0 0 3px var(--cn-select-ring), 0 3px 7px #00000080}.board-tile.is-hover-linked{outline:2px solid var(--color-accent-300);outline-offset:2px}.board-tile__content{width:100%;color:inherit;font:inherit;cursor:inherit;background:0 0;border:0;flex-direction:column;justify-content:flex-end;padding:clamp(5px,.7vw,8px);display:flex;position:absolute;inset:0}.board-tile__hole{background:var(--cn-card-hole);border-radius:50%;width:8px;height:8px;position:absolute;top:6px;left:50%;transform:translate(-50%);box-shadow:inset 0 1.5px 2.5px #5a46288c}.board-tile__role{color:var(--tile-role-icon,var(--tile-ring));font-size:.72rem;position:absolute;top:6px;left:7px}.board-tile__mirror{text-align:center;color:var(--cn-card-mirror);margin-bottom:2px;font-size:clamp(.52rem,.8vw,.72rem);font-weight:600;line-height:1;transform:rotate(180deg)}.board-tile.role-red .board-tile__mirror,.board-tile.role-blue .board-tile__mirror,.board-tile.role-assassin .board-tile__mirror{color:#ffffff94}.board-tile__label{text-overflow:ellipsis;white-space:nowrap;border:1px solid var(--cn-card-label-bd);border-radius:var(--radius-sm);background:var(--cn-card-label);min-width:0;color:var(--cn-card-ink);text-align:center;padding:3px 4px;font-size:clamp(.9rem,1.3vw,1.1rem);font-weight:700;line-height:1.25;overflow:hidden}.board-tile__badge{background:var(--cn-badge-bg);width:1.15rem;height:1.15rem;color:var(--cn-badge-ink);border-radius:50%;place-items:center;font-size:.65rem;font-weight:700;display:grid;position:absolute;top:4px;right:5px}.board-tile__lifecycle{z-index:3;color:#2d2515b3;max-width:calc(100% - 3.5rem);font:inherit;white-space:nowrap;opacity:0;cursor:pointer;background:#fffdf6b3;border:1px solid #2d251538;border-radius:999px;padding:1px 5px;font-size:clamp(.46rem,.6vw,.58rem);line-height:1.4;transition:opacity .12s;position:absolute;top:4px;left:50%;overflow:hidden;transform:translate(-50%)}.board-cell:hover .board-tile__lifecycle,.board-cell:focus-within .board-tile__lifecycle,.board-tile.is-chosen+.board-tile__lifecycle{opacity:1}.board-tile.is-chosen{background:var(--tile-gradient);cursor:default;box-shadow:inset 0 0 0 1px #ffffff14,0 2px 5px #0006}.board-tile.is-chosen .board-tile__content{opacity:.4}.board-tile.is-chosen .board-tile__hole,.board-tile.is-chosen .board-tile__mirror{display:none}.board-tile.is-chosen .board-tile__label{color:var(--cn-text);background:0 0;border:0;font-size:clamp(.85rem,1.15vw,1rem);text-decoration:line-through}.board-tile.is-chosen.role-neutral .board-tile__label{color:var(--cn-card-ink)}.board-tile.is-assassin-chosen{background:#030305}.board-tile.is-assassin-chosen .board-tile__label{color:#fff}.board-tile.is-chosen+.board-tile__lifecycle{color:var(--cn-text);background:#0a0a0f99;border-color:#fff3}.board-tile__chosen-chip{width:1.2rem;height:1.2rem;color:var(--cn-text);background:#0a0a0fa6;border-radius:50%;place-items:center;display:inline-grid;position:absolute;top:6px;left:7px}.board-tile__chosen-chip .cn-role-icon{font-size:.65rem}.board__hint{margin:var(--space-3) var(--space-2) 0;color:var(--cn-text-faint);font-size:.68rem}@media (width<=720px){.board__toolbar{flex-direction:column;align-items:flex-start}.board__toolbar-actions{flex-wrap:wrap;width:100%}.board__remaining{margin-inline-end:auto}.board__grid{grid-template-columns:repeat(5,minmax(7.5rem,1fr));overflow-x:auto}}@media (prefers-reduced-motion:reduce){.board-tile{transition:none}}.feedback-controls{gap:var(--space-3);border-block-start:1px solid var(--color-divider);color:var(--cn-text-muted);padding-block-start:var(--space-3);display:grid}.feedback-controls__gut-check,.feedback-controls__comment-actions,.feedback-controls__status,.feedback-controls__sent{align-items:center;display:flex}.feedback-controls__gut-check{gap:var(--space-2);min-height:2.25rem}.feedback-controls__prompt{margin-inline-end:var(--space-1);font-size:13px;font-weight:600}.feedback-controls__vote{border-radius:var(--radius-md);width:2.25rem;height:2.25rem;color:var(--color-accent-400);cursor:pointer;filter:grayscale();opacity:.78;background:0 0;border:1px solid #0000;padding:0;font-size:1rem;transition:border-color .14s,background .14s,filter .14s,opacity .14s}.feedback-controls__vote:hover:not(:disabled),.feedback-controls__vote[aria-pressed=true]{border-color:color-mix(in srgb, var(--color-accent) 60%, transparent);background:color-mix(in srgb, var(--color-accent) 10%, transparent);filter:none;opacity:1}.feedback-controls__vote:disabled:not([aria-pressed=true]){opacity:.28}.feedback-controls__details,.feedback-controls__comment-row{gap:var(--space-2);animation:.16s ease-out both feedback-reveal;display:grid}.feedback-controls__question,.feedback-controls__comment-row label{color:var(--cn-text-muted);margin:0;font-size:12px}.feedback-controls__reasons{gap:var(--space-2);flex-wrap:wrap;display:flex}.feedback-controls__reason{min-height:2rem;padding:var(--space-1) var(--space-3);border:1px solid color-mix(in srgb, var(--color-accent) 72%, transparent);border-radius:var(--radius-md);color:var(--color-accent-300);cursor:pointer;font:inherit;background:0 0;font-size:12px}.feedback-controls__reason:hover:not(:disabled),.feedback-controls__reason[aria-pressed=true]{background:color-mix(in srgb, var(--color-accent) 14%, transparent);border-color:var(--color-accent)}.feedback-controls__reason:disabled:not([aria-pressed=true]){opacity:.36}.feedback-controls__comment-actions{align-items:stretch;gap:var(--space-2)}.feedback-controls__comment{resize:vertical;min-height:3.5rem}.feedback-controls__comment-submit{flex:none}.feedback-controls__status,.feedback-controls__sent{gap:var(--space-2);min-height:2rem;margin:0;font-size:12px}.feedback-controls__sent{color:var(--cn-ok)}.feedback-controls__spinner{border:2px solid color-mix(in srgb, var(--color-accent) 28%, transparent);border-block-start-color:var(--color-accent);border-radius:50%;width:.9rem;height:.9rem;animation:.7s linear infinite feedback-spin}@keyframes feedback-reveal{0%{opacity:0;transform:translateY(calc(var(--space-1) * -1))}}@keyframes feedback-spin{to{transform:rotate(1turn)}}@media (prefers-reduced-motion:reduce){.feedback-controls__details,.feedback-controls__comment-row,.feedback-controls__spinner{animation:none}}.check-panel{gap:var(--space-6);display:grid}.check-panel__intro{gap:var(--space-2);padding:var(--space-4);border-inline-start:2px solid var(--color-accent);border-radius:var(--radius-sm);background:color-mix(in srgb, var(--color-accent) 7%, transparent);display:grid}.check-panel__intro p{color:var(--cn-text-muted);margin:0;font-size:.78rem;line-height:1.55}.check-panel__intro .check-panel__privacy{color:var(--color-accent-300);font-weight:700}.check-target{border:0;min-width:0;margin:0;padding:0}.check-target legend,.check-form label{color:var(--cn-text-muted);margin-block-end:var(--space-2);font-size:.78rem}.check-target__options{border:1px solid var(--color-divider);border-radius:var(--radius-md);grid-template-columns:repeat(2,minmax(0,1fr));display:grid;overflow:hidden}.check-target__option{justify-content:center;align-items:center;gap:var(--space-2);min-height:2.65rem;color:var(--cn-text-muted);cursor:pointer;display:flex;position:relative}.check-target__option+.check-target__option{border-inline-start:1px solid var(--color-divider)}.check-target__option input{opacity:0;pointer-events:none;position:absolute}.check-target__option:hover{background:color-mix(in srgb, var(--cn-role-dot) 7%, transparent)}.check-target__option.is-active{color:var(--cn-role-text);background:var(--cn-role-tint);box-shadow:inset 0 -2px 0 var(--cn-role-ring)}.check-target__option:has(input:focus-visible){outline:2px solid var(--color-accent);outline-offset:-2px}.check-form{gap:var(--space-3);display:grid}.check-form__input{box-sizing:border-box;width:100%;min-height:2.8rem;font-size:1rem}.check-form__validation{margin:calc(var(--space-1) * -1) 0 0;color:var(--cn-red-text);font-size:.75rem}.check-form__submit{width:100%}.check-result{gap:var(--space-4);display:grid}.check-illegal{gap:var(--space-1);padding:var(--space-4);border:1px solid var(--cn-red-ring);border-radius:var(--radius-md);background:var(--cn-red-tint);color:var(--cn-red-text);font-size:.78rem;line-height:1.5;display:grid}.check-summary{gap:var(--space-3);padding:var(--space-4);border:1px solid var(--color-divider);border-radius:var(--radius-md);background:var(--cn-panel);display:grid}.check-summary p{color:var(--cn-text-muted);margin:0;font-size:.78rem;line-height:1.55}.check-summary__safe{padding:var(--space-2) var(--space-3);border:1px solid color-mix(in srgb, var(--cn-ok) 48%, transparent);background:color-mix(in srgb, var(--cn-ok) 10%, transparent);color:var(--cn-ok);border-radius:999px;justify-self:start;font-size:.8rem;font-weight:700}.check-summary .check-summary__assassin{align-items:center;gap:var(--space-2);display:flex}.check-summary .check-summary__assassin.is-close{padding:var(--space-3);border:1px solid var(--cn-red-ring);border-radius:var(--radius-sm);background:var(--cn-red-tint);color:var(--cn-red-text);font-weight:700}.check-ranked{background:var(--cn-panel)}.check-ranked__scale{color:var(--cn-text-dim);white-space:nowrap;font-size:.7rem}.check-ranked__list{gap:var(--space-1);scrollbar-color:var(--color-neutral-700) transparent;max-height:23rem;margin:0;padding:0;list-style:none;display:grid;overflow:auto}.check-ranked__row{align-items:center;gap:var(--space-3);min-height:2.25rem;padding-inline:var(--space-2);border-radius:var(--radius-sm);grid-template-columns:1.5rem minmax(4.5rem,auto) minmax(4rem,1fr) 2rem;font-size:.76rem;display:grid}.check-ranked__row:hover,.check-ranked__row:focus-visible{background:color-mix(in srgb, var(--cn-role-dot) 8%, transparent);outline:1px solid color-mix(in srgb, var(--cn-role-ring) 55%, transparent)}.check-ranked__rank{color:var(--cn-text-dim);font-variant-numeric:tabular-nums}.check-ranked__word{align-items:center;gap:var(--space-2);min-width:0;color:var(--cn-text);font-weight:600;display:flex}.check-ranked__bar{background:color-mix(in srgb, var(--cn-text) 7%, transparent);border-radius:999px;height:.34rem;overflow:hidden}.check-ranked__bar>span{border-radius:inherit;background:var(--cn-role-dot);height:100%;display:block}.check-ranked__row strong{color:var(--cn-role-text);font-variant-numeric:tabular-nums;text-align:end}.check-ranked__help,.check-ranked__empty{margin:var(--space-3) 0 0;color:var(--cn-text-dim);font-size:.7rem;line-height:1.5}@media (width<=1100px){.check-ranked__row{gap:var(--space-2);grid-template-columns:1.25rem minmax(4rem,auto) minmax(3rem,1fr) 1.8rem}}.clue-panel{gap:var(--space-4);color:var(--cn-text);display:grid}.clue-builder,.clue-result,.clue-placeholder{border:1px solid var(--color-divider);border-radius:var(--radius-md);background:color-mix(in srgb, var(--cn-panel) 82%, transparent)}.clue-builder{gap:var(--space-4);padding:var(--space-4);display:grid}.clue-builder__heading{justify-content:space-between;align-items:end;gap:var(--space-4);display:flex}.clue-builder h2{margin:0;font-size:1rem;font-weight:600}.clue-panel__eyebrow{margin:0 0 var(--space-1);color:var(--color-accent-400);letter-spacing:.04em;font-size:.7rem;font-weight:600}.clue-target,.clue-risk__options{background:color-mix(in srgb, var(--cn-app-bg) 35%, transparent);width:fit-content;max-width:100%;display:inline-flex}.clue-target__option,.clue-risk__option{min-height:2.25rem;padding:var(--space-2) var(--space-3);color:var(--cn-text-muted);cursor:pointer;font:inherit;background:0 0;border:0;font-size:.8rem}.clue-target__option{justify-content:center;align-items:center;gap:var(--space-2);display:inline-flex}.clue-target__option+.clue-target__option,.clue-risk__option+.clue-risk__option{border-inline-start:1px solid var(--color-divider)}.clue-target__option[aria-pressed=true],.clue-risk__option[aria-pressed=true]{color:var(--color-accent-300);box-shadow:inset 0 0 0 1px var(--color-accent)}.clue-target__option:disabled,.clue-risk__option:disabled{cursor:not-allowed;opacity:.5}.clue-selection{gap:var(--space-3);padding-block:var(--space-3);border-block:1px solid var(--color-divider);display:grid}.clue-selection__summary{justify-content:space-between;align-items:center;gap:var(--space-2);color:var(--cn-text-muted);flex-wrap:wrap;font-size:.75rem;display:flex}.clue-selection__color{align-items:center;gap:var(--space-1);color:var(--cn-role-text);display:inline-flex}.clue-chip-list{gap:var(--space-2);flex-wrap:wrap;display:flex}.clue-chip{align-items:center;gap:var(--space-1);padding:var(--space-1) var(--space-3);border:1px solid color-mix(in srgb, var(--cn-role-ring,var(--color-divider)) 60%, transparent);background:var(--cn-role-tint,color-mix(in srgb, var(--cn-text) 5%, transparent));color:var(--cn-role-text,var(--cn-text));border-radius:999px;font-size:.74rem;line-height:1.4;display:inline-flex}.clue-chip:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}.clue-risk{gap:var(--space-2);border:0;min-width:0;margin:0;padding:0;display:grid}.clue-risk legend{color:var(--cn-text-muted);padding:0;font-size:.77rem}.clue-risk__options{justify-self:start}.clue-risk__help{color:var(--cn-text-dim);margin:0;font-size:.69rem;line-height:1.55}.clue-actions{gap:var(--space-3);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.clue-actions__button{width:100%;min-height:2.7rem;padding-inline:var(--space-2);white-space:normal}.clue-placeholder{align-items:center;gap:var(--space-4);padding:var(--space-4);color:var(--cn-text-muted);display:flex}.clue-placeholder>span{color:var(--color-accent-400);font-size:1.35rem}.clue-placeholder strong{color:var(--cn-text);font-size:.85rem}.clue-placeholder p{margin:var(--space-1) 0 0;font-size:.72rem}.clue-result{gap:var(--space-4);min-width:0;padding:var(--space-4);display:grid;position:relative;overflow:hidden}.clue-result__header{justify-content:space-between;align-items:start;gap:var(--space-4);display:flex}.clue-result__title-row{align-items:center;gap:var(--space-3);display:flex}.clue-result__title-row h3{color:var(--cn-text);margin:0;font-size:clamp(2rem,3vw,2.8rem);font-weight:500;line-height:1}.clue-result__count{padding:var(--space-1) var(--space-3);border:1px solid var(--color-accent-600);color:var(--color-accent-300);border-radius:999px;font-size:.75rem}.clue-intended{gap:var(--space-2);color:var(--cn-text-muted);font-size:.72rem;display:grid}.clue-reason{color:var(--cn-text-muted);margin:0;font-size:.8rem;line-height:1.65}.clue-warning,.clue-empty,.clue-stale{border:1px solid var(--cn-warn-bd);border-radius:var(--radius-md);background:var(--cn-warn-bg);color:var(--cn-warn)}.clue-warning,.clue-empty{padding:var(--space-3)}.clue-warning strong,.clue-empty strong{font-size:.8rem}.clue-warning p,.clue-empty p{margin:var(--space-2) 0 0;font-size:.74rem}.clue-warning .clue-chip-list{margin-top:var(--space-3)}.clue-assassin{align-items:center;gap:var(--space-2);color:var(--cn-text-muted);margin:0;font-size:.74rem;display:flex}.clue-ranking{gap:var(--space-3);min-width:0;display:grid}.clue-ranking__header{justify-content:space-between;align-items:baseline;gap:var(--space-3);display:flex}.clue-ranking__header h4{margin:0;font-size:.78rem;font-weight:600}.clue-ranking__header span{color:var(--cn-text-dim);font-size:.66rem}.clue-ranking__list{gap:var(--space-2);margin:0;padding:0;list-style:none;display:grid}.clue-ranking__row{align-items:center;gap:var(--space-2);grid-template-columns:minmax(4.5rem,auto) minmax(3rem,1fr) 2rem;font-size:.7rem;display:grid}.clue-ranking__word{align-items:center;gap:var(--space-1);text-overflow:ellipsis;white-space:nowrap;display:inline-flex;overflow:hidden}.clue-ranking__track{background:color-mix(in srgb, var(--cn-text) 8%, transparent);border-radius:999px;height:4px;overflow:hidden}.clue-ranking__fill{border-radius:inherit;background:var(--cn-role-dot);height:100%;display:block}.clue-ranking__score{color:var(--cn-text-dim);font-variant-numeric:tabular-nums;text-align:end}.clue-use{align-items:center;gap:var(--space-3);padding-top:var(--space-3);border-top:1px solid var(--color-divider);flex-wrap:wrap;display:flex}.clue-use__confirmation{color:var(--cn-ok);font-size:.72rem}.clue-carousel{align-items:center;gap:var(--space-2);padding-top:var(--space-3);border-top:1px solid var(--color-divider);color:var(--cn-text-muted);grid-template-columns:1fr auto 1fr;font-size:.7rem;display:grid}.clue-carousel>:last-child{justify-self:end}.clue-stale{z-index:5;justify-content:space-between;align-items:center;gap:var(--space-4);padding:var(--space-4);background:color-mix(in srgb, var(--cn-app-bg) 94%, transparent);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;position:absolute;inset:0}.clue-stale div{gap:var(--space-2);display:grid}.clue-stale strong{font-size:.82rem}.clue-stale span{color:var(--cn-text-muted);font-size:.72rem}@media (width<=1100px){.clue-builder__heading,.clue-stale{flex-direction:column;align-items:stretch}.clue-actions{grid-template-columns:minmax(0,1fr)}}@media (prefers-reduced-motion:reduce){.clue-stale{-webkit-backdrop-filter:none;backdrop-filter:none}}.session-log{border:1px solid var(--color-divider);background:color-mix(in srgb, var(--cn-sheet) 88%, transparent);padding:0;overflow:hidden}.session-log>.cn-panel__header{display:block}.session-log__toggle{align-items:center;gap:var(--space-2);width:100%;min-height:2.75rem;padding:var(--space-3) var(--space-4);color:var(--cn-text);cursor:pointer;font:inherit;text-align:start;background:0 0;border:0;font-size:14px;font-weight:600;display:flex}.session-log__toggle:hover{background:color-mix(in srgb, var(--cn-text) 4%, transparent)}.session-log__total{border:1px solid color-mix(in srgb, var(--color-accent) 52%, transparent);min-width:1.35rem;height:1.35rem;color:var(--color-accent-300);border-radius:50%;place-items:center;font-size:11px;display:inline-grid}.session-log__chevron{color:var(--cn-text-muted);margin-inline-start:auto;transition:transform .16s}.session-log__toggle[aria-expanded=false] .session-log__chevron{transform:rotate(90deg)}.session-log .cn-panel__body{border-block-start:1px solid var(--color-divider)}.session-log__empty{padding:var(--space-4);color:var(--cn-text-muted);margin:0;font-size:12px}.session-log__list{max-height:22rem;overflow-y:auto}.session-log__entry{gap:var(--space-2);padding:var(--space-4);display:grid}.session-log__entry+.session-log__entry{border-block-start:1px solid var(--color-divider)}.session-log__entry-header,.session-log__clue-line,.session-log__meta,.session-log__intended,.session-log__outcome,.session-log__team,.session-log__reveal{align-items:center;display:flex}.session-log__entry-header{justify-content:space-between;gap:var(--space-3)}.session-log__clue-line{gap:var(--space-2)}.session-log__clue-line strong{color:var(--cn-text);font-size:16px}.session-log__count{border:1px solid var(--color-accent);width:1.6rem;height:1.6rem;color:var(--color-accent-300);border-radius:50%;place-items:center;font-size:12px;display:inline-grid}.session-log__entry time,.session-log__meta,.session-log__pending{color:var(--cn-text-muted);font-size:11px}.session-log__meta,.session-log__intended,.session-log__outcome{gap:var(--space-2);flex-wrap:wrap}.session-log__team,.session-log__reveal{gap:var(--space-1)}.session-log__repeat{color:var(--cn-warn)}.session-log__reveal{padding:var(--space-1) var(--space-2);border-radius:var(--radius-sm);border:1px solid;font-size:11px}.session-log__reveal--correct{color:var(--cn-ok);background:color-mix(in srgb, var(--cn-ok) 8%, transparent)}.session-log__reveal--miss{color:var(--cn-warn);background:var(--cn-warn-bg)}.session-log__reveal--danger{color:var(--cn-red-text);background:color-mix(in srgb, var(--cn-red-dot) 10%, transparent)}@media (prefers-reduced-motion:reduce){.session-log__chevron{transition:none}}.semantic-panel{border:1px solid var(--color-divider);border-radius:var(--radius-lg);background:var(--cn-panel);min-width:0;overflow:hidden;box-shadow:0 14px 34px #0000003d}.semantic-panel__header{min-height:4.25rem;padding:var(--space-4) var(--space-6);justify-content:space-between;align-items:center;gap:var(--space-6);border-bottom:1px solid var(--color-divider);background:color-mix(in srgb, var(--cn-surface-2) 82%, transparent);display:flex}.semantic-panel__header h2,.semantic-panel__header p{margin:0}.semantic-panel__header h2{font-size:1.05rem}.semantic-panel__eyebrow{color:var(--color-accent-400);letter-spacing:.04em;font-size:.68rem;font-weight:700;margin-block-end:var(--space-1)!important}.semantic-panel__clue{padding:var(--space-2) var(--space-4);border:1px solid var(--color-accent-700);background:color-mix(in srgb, var(--color-accent) 10%, transparent);color:var(--color-accent-200);border-radius:999px;font-size:.76rem}.semantic-panel__clue.is-empty{border-color:var(--color-divider);color:var(--cn-text-dim);background:0 0}.semantic-map__frame{min-height:28rem;padding:var(--space-4);background:var(--cn-board-void);place-items:center;display:grid;position:relative}.semantic-map{direction:rtl;width:min(100%,48rem);max-height:min(72vh,48rem);display:block;overflow:visible}.semantic-map__background{fill:var(--cn-map-bg);stroke:var(--color-divider);stroke-width:1.5px}.semantic-map__grid{fill:url(#semantic-grid);opacity:.56}.semantic-map__grid-line{fill:none;stroke:var(--cn-map-grid);stroke-width:1px}.semantic-map__target-line{stroke:var(--color-accent-500);stroke-width:2px;stroke-linecap:round;opacity:.7}.semantic-map__dot{cursor:pointer;outline:none}.semantic-map__dot-hit-area{fill:#0000;pointer-events:all}.semantic-map__dot-circle{fill:var(--cn-map-bg);stroke:var(--cn-role-dot);stroke-width:2.5px;transition:r .12s,filter .12s}.semantic-map__dot.is-target .semantic-map__dot-circle{fill:var(--cn-role-dot);filter:drop-shadow(0 0 8px var(--cn-role-tint))}.semantic-map__dot:hover .semantic-map__dot-circle,.semantic-map__dot:focus-visible .semantic-map__dot-circle,.semantic-map__dot.is-linked .semantic-map__dot-circle{filter:drop-shadow(0 0 7px var(--cn-role-dot))}.semantic-map__dot-shape{fill:var(--cn-role-dot);pointer-events:none;font-size:8px;font-weight:800}.semantic-map__dot.is-target .semantic-map__dot-shape{fill:var(--cn-map-bg)}.semantic-map__word-label{fill:var(--cn-role-text);font-family:var(--font-hebrew);paint-order:stroke;stroke:var(--cn-map-bg);stroke-width:4px;stroke-linejoin:round;pointer-events:none;font-size:12px;font-weight:700}.semantic-map__linked-ring{fill:none;stroke:var(--color-accent-300);stroke-width:2px;stroke-dasharray:3 3;pointer-events:none}.semantic-map__danger{fill:none;stroke:var(--cn-warn);stroke-width:2px;opacity:.9;transform-box:fill-box;transform-origin:50%;pointer-events:none;animation:1.8s ease-in-out infinite semantic-danger-pulse}.semantic-map__danger.is-assassin{stroke:var(--cn-red-dot);stroke-width:3.5px;stroke-dasharray:4 3}.semantic-map__hint{pointer-events:none}.semantic-map__hint-glow{fill:url(#semantic-hint-glow)}.semantic-map__hint-node{fill:var(--color-accent-300);stroke:var(--cn-map-hint);stroke-width:4px;filter:drop-shadow(0 0 9px var(--cn-map-hint-glow))}.semantic-map__hint-label{fill:var(--color-accent-100);font-family:var(--font-hebrew);paint-order:stroke;stroke:var(--cn-map-bg);stroke-width:5px;stroke-linejoin:round;font-size:21px;font-weight:800}.semantic-map__loading{justify-content:center;align-items:center;gap:var(--space-4);background:color-mix(in srgb, var(--cn-map-bg) 86%, transparent);color:var(--cn-text-muted);font-size:.8rem;display:flex;position:absolute;inset:0}.semantic-map__loading .cn-loading-spinner{width:1rem;height:1rem;position:static;transform:none}.semantic-map__tooltip{min-height:2.6rem;padding:var(--space-3) var(--space-4);align-items:center;gap:var(--space-3);border:1px solid color-mix(in srgb, var(--cn-role-ring) 75%, var(--color-divider));border-radius:var(--radius-md);background:color-mix(in srgb, var(--cn-sheet) 94%, transparent);box-shadow:var(--shadow-md);color:var(--cn-text);pointer-events:none;font-size:.76rem;display:flex;position:absolute;inset-block-end:var(--space-6);inset-inline-start:50%;transform:translate(50%)}.semantic-map__tooltip span{color:var(--cn-text-muted)}.semantic-map__legend{min-height:3rem;padding:var(--space-3) var(--space-6);align-items:center;gap:var(--space-3);border-top:1px solid var(--color-divider);color:var(--cn-text-muted);flex-wrap:wrap;font-size:.72rem;line-height:1.4;display:flex}.semantic-map__legend strong{color:var(--cn-text)}.semantic-map__legend-role{align-items:center;gap:var(--space-1);color:var(--cn-role-text);display:inline-flex}.semantic-map__legend-hintless{color:var(--cn-text-dim);flex-basis:100%}@keyframes semantic-danger-pulse{0%,to{opacity:.55;transform:scale(.94)}50%{opacity:1;transform:scale(1.08)}}@media (prefers-reduced-motion:reduce){.semantic-map__danger{animation:none}.semantic-map__dot-circle{transition:none}}@media (width<=900px){.semantic-map__frame{min-height:22rem;padding:var(--space-3)}.semantic-panel__header,.semantic-map__legend{padding-inline:var(--space-4)}}.photo-setup{gap:var(--space-8);flex-direction:column;width:min(100%,1240px);min-height:min(820px,100svh - 44.8px);display:flex}.photo-setup__header,.photo-setup__section-title,.photo-setup__key-tools,.photo-setup__counts,.photo-setup__key-upload,.photo-setup__manual-note{align-items:center;display:flex}.photo-setup__header{justify-content:space-between;gap:var(--space-8)}.photo-setup__header h1{font-size:clamp(1.65rem,3vw,2.35rem)}.photo-setup__header p,.photo-setup__section-title p,.photo-setup__uploads>p{color:var(--cn-text-muted)}.photo-setup__eyebrow{margin:0 0 var(--space-2);font-size:.75rem}.photo-setup__modes{border:1px solid var(--color-divider);border-radius:var(--radius-md);grid-template-columns:repeat(3,minmax(7.5rem,1fr));display:grid;overflow:hidden}.photo-setup__modes button{min-height:3rem;padding:0 var(--space-6);border:0;border-inline-start:1px solid var(--color-divider);color:var(--cn-text-muted);font:inherit;cursor:pointer;background:0 0}.photo-setup__modes button:last-child{border-inline-start:0}.photo-setup__modes button:hover:not(:disabled){background:color-mix(in srgb, var(--cn-text) 7%, transparent)}.photo-setup__modes button.is-active{color:var(--color-accent-300);background:color-mix(in srgb, var(--color-accent) 9%, transparent);box-shadow:inset 0 0 0 1px var(--color-accent)}.photo-setup__modes button:disabled{opacity:.55;cursor:wait}.photo-setup__body{gap:var(--space-8);grid-template-columns:minmax(0,2.25fr) minmax(18rem,.9fr);align-items:start;display:grid}.photo-setup__editor,.photo-setup__uploads{border:1px solid var(--color-divider);border-radius:var(--radius-lg);background:var(--cn-panel);padding:var(--space-8)}.photo-setup__section-title{justify-content:space-between;gap:var(--space-6);margin-bottom:var(--space-6)}.photo-setup__section-title h2,.photo-setup__uploads h2{font-size:1.15rem}.photo-setup__section-title p,.photo-setup__uploads>p{margin:0;font-size:.78rem}.photo-setup__key-status{color:var(--cn-warn);white-space:nowrap;font-size:.78rem}.photo-setup__key-status.is-valid{color:var(--cn-ok)}.photo-setup__word-grid{padding:var(--space-4);border-radius:var(--radius-lg);background:var(--cn-board-well);direction:rtl;grid-template-columns:repeat(5,minmax(0,1fr));gap:clamp(5px,.65vw,9px);display:grid}.photo-setup__word-cell{--setup-role:var(--cn-neutral-ring);--setup-face:var(--cn-card-face);--setup-icon:var(--cn-neutral-icon);aspect-ratio:1.5;border-radius:var(--radius-md);background:var(--setup-face);min-width:0;box-shadow:inset 0 0 0 2px var(--setup-role), inset 0 1.5px 0 #ffffff73, 0 2px 5px #0006;border:0;flex-direction:column;justify-content:flex-end;padding:clamp(5px,.65vw,8px);display:flex;position:relative;overflow:hidden}.photo-setup__word-cell.role-red{--setup-role:var(--cn-red-ring);--setup-face:var(--cn-red-grad);--setup-icon:var(--cn-red-text)}.photo-setup__word-cell.role-blue{--setup-role:var(--cn-blue-ring);--setup-face:var(--cn-blue-grad);--setup-icon:var(--cn-blue-text)}.photo-setup__word-cell.role-neutral{--setup-role:var(--cn-neutral-ring);--setup-icon:var(--cn-neutral-icon)}.photo-setup__word-cell.role-assassin{--setup-role:var(--cn-assassin-dot);--setup-face:var(--cn-assassin-grad);--setup-icon:var(--cn-assassin-dot)}.photo-setup__word-cell.is-low-confidence{box-shadow:0 0 0 2px var(--cn-warn)}.photo-setup__word-cell:focus-within{box-shadow:0 0 0 2px var(--color-accent)}.photo-setup__word-cell input{z-index:2;border:1px solid var(--cn-card-label-bd);border-radius:var(--radius-sm);background:var(--cn-card-label);width:100%;min-width:0;color:var(--cn-card-ink);font:inherit;text-align:center;direction:rtl;padding:3px 4px;font-size:clamp(.72rem,1vw,.9rem);font-weight:700;line-height:1.25;position:relative}.photo-setup__word-cell input:focus{outline:0}.photo-setup__word-hole{background:var(--cn-card-hole);border-radius:50%;width:7px;height:7px;position:absolute;top:5px;left:50%;transform:translate(-50%);box-shadow:inset 0 1px 2px #5a46288c}.photo-setup__word-mirror{color:var(--cn-card-mirror);text-align:center;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px;font-size:clamp(.5rem,.7vw,.66rem);font-weight:600;line-height:1;overflow:hidden;transform:rotate(180deg)}.photo-setup__word-cell.role-red .photo-setup__word-mirror,.photo-setup__word-cell.role-blue .photo-setup__word-mirror,.photo-setup__word-cell.role-assassin .photo-setup__word-mirror{color:#ffffff94}.photo-setup__word-role{z-index:3;border-radius:var(--radius-sm);width:1.45rem;height:1.45rem;color:var(--setup-icon);cursor:pointer;background:#0a0a0f1f;border:0;place-items:center;padding:0;display:grid;position:absolute;top:5px;left:6px}.photo-setup__word-role:hover{background:#0a0a0f3d}.photo-setup__validation{color:var(--cn-warn);margin:var(--space-3) 0 0;font-size:.82rem}.photo-setup__key-tools{justify-content:space-between;gap:var(--space-4);margin-bottom:var(--space-4)}.photo-setup__counts{gap:var(--space-4);color:var(--cn-text-muted);flex-wrap:wrap;font-size:.75rem}.photo-setup__counts span{align-items:center;gap:var(--space-2);display:inline-flex}.photo-setup__counts .role-red{color:var(--cn-red-text)}.photo-setup__counts .role-blue{color:var(--cn-blue-text)}.photo-setup__counts .role-neutral{color:var(--cn-neutral-text)}.photo-setup__confirm{margin-top:var(--space-6);min-height:2.75rem;margin-inline-start:auto}.photo-setup__uploads{transition:border-color .2s,box-shadow .2s}.photo-setup__uploads.is-emphasized{border-color:var(--color-accent-700);box-shadow:0 0 18px #968ae017}.photo-setup__drop-zone{min-height:12.5rem;margin-top:var(--space-6);border:1px dashed var(--color-neutral-700);border-radius:var(--radius-lg);justify-content:center;align-items:center;gap:var(--space-2);text-align:center;cursor:pointer;flex-direction:column;display:flex;overflow:hidden}.photo-setup__drop-zone:hover{border-color:var(--color-accent)}.photo-setup__drop-zone img{object-fit:cover;opacity:.65;width:100%;height:7.5rem}.photo-setup__drop-zone small,.photo-setup__key-upload small{color:var(--cn-text-dim)}.photo-setup__upload-icon{color:var(--cn-text-dim);font-size:2.3rem;line-height:1}.photo-setup__drop-zone input,.photo-setup__key-upload input{opacity:0;width:1px;height:1px;position:absolute}.photo-setup__ocr-status{align-items:center;gap:var(--space-3);min-height:2rem;color:var(--cn-text-muted);font-size:.72rem;display:flex}.photo-setup__skip{min-height:1.75rem;font-size:.68rem}.photo-setup__ocr-status.is-success,.photo-setup__ocr-status.is-ready{color:var(--cn-ok)}.photo-setup__ocr-status.is-error{color:var(--cn-warn)}.photo-setup__spinner{border:2px solid var(--color-neutral-700);border-top-color:var(--color-accent);border-radius:50%;width:1rem;height:1rem;animation:.8s linear infinite photo-spin}@keyframes photo-spin{to{transform:rotate(360deg)}}.photo-setup__key-upload{justify-content:space-between;gap:var(--space-3);padding:var(--space-4);border:1px solid var(--color-divider);border-radius:var(--radius-md);cursor:pointer;position:relative}.photo-setup__key-upload span{flex-direction:column;display:flex}.photo-setup__key-upload img{border-radius:var(--radius-sm);object-fit:cover;width:3rem;height:3rem}.photo-setup__manual-note{gap:var(--space-3);margin-top:var(--space-6);padding:var(--space-4);color:var(--color-accent-300);border:1px solid var(--color-accent-800);border-radius:var(--radius-md);background:color-mix(in srgb, var(--color-accent) 7%, transparent)}.photo-setup__manual-note p{margin:0;font-size:.78rem}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}@media (width<=960px){.photo-setup__body{grid-template-columns:1fr}.photo-setup__header{flex-direction:column;align-items:stretch}}@media (width<=620px){.photo-setup__word-grid{gap:4px;padding:6px}.photo-setup__word-cell input{padding:2px 3px;font-size:.72rem}.photo-setup__modes{grid-template-columns:repeat(3,1fr)}.photo-setup__modes button{min-width:0;padding:0 4px;font-size:.75rem}}@media (prefers-reduced-motion:reduce){.photo-setup__spinner{animation:none}}.setup-shell,.main-screen{background:var(--cn-app-bg);min-height:100svh;color:var(--cn-text)}.setup-shell{padding:var(--space-8);place-items:center;display:grid}.main-screen{flex-direction:column;display:flex}.main-screen__header{min-height:4.5rem;padding:var(--space-4) clamp(var(--space-6), 3vw, 2.5rem);border-bottom:1px solid var(--color-divider);background:var(--color-bg);justify-content:space-between;align-items:center;gap:var(--space-8);display:flex}.main-screen__header h1,.main-screen__header p{margin:0}.main-screen__header h1{font-size:1.1rem;font-weight:700}.main-screen__eyebrow,.main-screen__status{color:var(--cn-text-muted);font-size:.75rem}.main-screen__eyebrow{margin-bottom:var(--space-1)!important}.main-screen__header-end{align-items:center;gap:var(--space-4);display:flex}.main-screen__methods{color:var(--cn-text-muted);white-space:nowrap;border-bottom:1px solid;padding-bottom:1px;font-size:.75rem;text-decoration:none}.main-screen__methods:hover{filter:brightness(1.4);text-decoration:none}.main-screen__workspace{flex:1;grid-template-columns:minmax(0,2.25fr) minmax(19rem,.85fr);min-height:0;display:grid}.main-screen__primary{min-width:0;padding:clamp(var(--space-4), 2vw, var(--space-8));align-content:start;gap:var(--space-6);background:var(--cn-board-void);display:grid}.main-screen__controls{min-width:0;padding:var(--space-6);border-inline-start:1px solid var(--color-divider);background:var(--cn-surface-2);gap:var(--space-6);flex-direction:column;display:flex}.main-screen__tabs{border:1px solid var(--color-divider);border-radius:var(--radius-md);grid-template-columns:repeat(2,minmax(0,1fr));display:grid;overflow:hidden}.main-screen__tabs button{min-height:2.75rem;color:var(--cn-text-muted);font:inherit;cursor:pointer;background:0 0;border:0;font-weight:600}.main-screen__tabs button+button{border-inline-start:1px solid var(--color-divider)}.main-screen__tabs button.is-active{color:var(--color-accent-300);box-shadow:inset 0 -2px 0 var(--color-accent)}.main-screen__tabs button:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}.main-screen__active-panel{min-height:12rem}@media (width<=900px){.main-screen__workspace{grid-template-columns:minmax(0,1fr)}.main-screen__controls{border-inline-start:0;border-top:1px solid var(--color-divider)}}:root{--color-bg:#161826;--color-surface:#232532;--color-text:#e9e9ed;--color-accent:#9184d9;--color-accent-2:#a7a1db;--color-divider:#e9e9ed29;--color-neutral-100:#f3f5fe;--color-neutral-200:#e4e7f5;--color-neutral-300:#cfd3e5;--color-neutral-400:#b2b6ca;--color-neutral-500:#9397ab;--color-neutral-600:#75798c;--color-neutral-700:#595d6c;--color-neutral-800:#3f424d;--color-neutral-900:#292b31;--color-accent-100:#f5f4ff;--color-accent-200:#e7e5fe;--color-accent-300:#d2cefd;--color-accent-400:#b5abfc;--color-accent-500:#968ae0;--color-accent-600:#796cbf;--color-accent-700:#5d5294;--color-accent-800:#423a6a;--color-accent-900:#2b2741;--color-accent-2-100:#f5f4ff;--color-accent-2-200:#e7e5fe;--color-accent-2-300:#d2cefd;--color-accent-2-400:#b5afe8;--color-accent-2-500:#9690c9;--color-accent-2-600:#7972a9;--color-accent-2-700:#5c5783;--color-accent-2-800:#423e5d;--color-accent-2-900:#2b293a;--color-section:#262a60;--color-section-glow:#353b80;--color-section-ghost:#4c5397;--font-heading:"Inter", system-ui, sans-serif;--font-heading-weight:500;--font-body:"Inter", system-ui, sans-serif;--space-1:2.8px;--space-2:5.6px;--space-3:8.4px;--space-4:11.2px;--space-6:16.8px;--space-8:22.4px;--radius-sm:4px;--radius-md:8px;--radius-lg:14px;--shadow-sm:0 0 0 1px #3f424d;--shadow-md:0 0 0 1px #595d6c, 0 6px 18px #0000008c;--shadow-lg:0 0 0 1px #9397ab, 0 16px 40px #000000a6}body{background:var(--color-bg);color:var(--color-text);font-family:var(--font-body)}h1,h2,h3,h4{font-family:var(--font-heading);font-weight:var(--font-heading-weight)}.lighten{mix-blend-mode:lighten;background-color:#0000}*,:before,:after{box-sizing:border-box}body{margin:0;font-size:15px;font-weight:400;line-height:1.55}h1,h2,h3,h4,h5,h6{font-family:var(--font-heading);font-weight:var(--font-heading-weight);letter-spacing:-.015em;margin:0 0 var(--space-2);line-height:1.12}h1{font-size:42px}h2{font-size:32px}h3{font-size:25px}h4{font-size:20px}h5{font-size:16px}h6{letter-spacing:.08em;text-transform:uppercase;font-size:13px}p{margin:0 0 var(--space-3)}a{color:var(--color-accent);text-underline-offset:3px}img{max-width:100%;display:block}figure{margin:0}figcaption{margin-top:var(--space-1);color:color-mix(in srgb, var(--color-text) 55%, transparent);font-size:11px}.text-muted{color:color-mix(in srgb, var(--color-text) 55%, transparent)}:focus{outline:none}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}::selection{background:color-mix(in srgb, var(--color-accent) 30%, transparent)}.hr{height:1px;margin:var(--space-4) 0;background:linear-gradient(to right, transparent, var(--color-divider) 48px, var(--color-divider) calc(100% - 48px), transparent);border:0}.btn{cursor:pointer;font-family:var(--font-heading);font-weight:var(--font-heading-weight);color:var(--color-text);padding:var(--space-2) calc(var(--space-3) * 1.2);border-radius:var(--radius-md);background:0 0;border:1px solid #0000;justify-content:center;align-items:center;gap:6px;font-size:14px;line-height:1.2;text-decoration:none;display:inline-flex}.btn svg{display:block}.btn:disabled{opacity:.45;cursor:not-allowed}.btn-primary{color:var(--color-accent);border-color:var(--color-accent)}.btn-primary:hover{background:color-mix(in srgb, var(--color-accent) 12%, transparent)}.btn-primary:active{background:color-mix(in srgb, var(--color-accent) 22%, transparent)}.btn-secondary{border-color:var(--color-divider)}.btn-secondary:hover{background:color-mix(in srgb, var(--color-text) 7%, transparent)}.btn-secondary:active{background:color-mix(in srgb, var(--color-text) 14%, transparent)}.btn-ghost{color:var(--color-accent);padding-inline:var(--space-1)}.btn-ghost:hover{background:color-mix(in srgb, var(--color-accent) 10%, transparent)}.btn-ghost:active{background:color-mix(in srgb, var(--color-accent) 18%, transparent)}.btn-icon{width:36px;height:36px;padding:0}.btn-block{width:100%;margin-top:var(--space-2)}.field>label{color:color-mix(in srgb, var(--color-text) 70%, transparent);margin-bottom:5px;font-size:12px;display:block}.input{width:100%;min-height:36px;font:inherit;color:var(--color-text);caret-color:var(--color-accent);background:var(--color-surface);border:1px solid var(--color-divider);border-radius:var(--radius-md);padding:6px 10px;font-size:14px}.input:hover{border-color:color-mix(in srgb, var(--color-text) 45%, transparent)}.input:focus-visible{border-color:var(--color-accent);outline-offset:0}textarea.input{resize:vertical;min-height:90px}.radio{cursor:pointer;align-items:center;gap:8px;font-size:14px;display:inline-flex}.radio input,.seg-opt input{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.radio .dot{border:1.5px solid var(--color-divider);border-radius:50%;flex:none;width:16px;height:16px}.radio:hover .dot{border-color:var(--color-accent)}.radio input:checked+.dot{border-color:var(--color-accent);background:var(--color-accent);box-shadow:inset 0 0 0 4px var(--color-bg)}.radio input:focus-visible+.dot{outline:2px solid var(--color-accent);outline-offset:2px}.seg{border:1px solid var(--color-divider);border-radius:var(--radius-md);display:inline-flex;overflow:hidden}.seg-opt{cursor:pointer;align-items:center;gap:6px;padding:7px 12px;font-size:13px;display:inline-flex}.seg-opt+.seg-opt{border-left:1px solid var(--color-divider)}.seg-opt:has(input:checked){color:var(--color-accent);box-shadow:inset 0 0 0 1px var(--color-accent)}.seg-opt:not(:has(input:checked)):hover{background:color-mix(in srgb, var(--color-text) 7%, transparent)}.seg-opt:has(input:focus-visible){outline:2px solid var(--color-accent);outline-offset:-2px}.card{gap:var(--space-2);padding:var(--space-3);border-radius:var(--radius-md);background:var(--color-surface);flex-direction:column;display:flex}.card-kicker{letter-spacing:.1em;text-transform:uppercase;color:var(--color-accent);font-size:10px}.card-title{font-family:var(--font-heading);font-weight:var(--font-heading-weight);font-size:17px;line-height:1.2}.card-body{opacity:.8;flex:1;margin:0;font-size:13px}.card-meta{color:color-mix(in srgb, var(--color-text) 50%, transparent);align-items:center;gap:6px;font-size:11px;display:flex}.elev-sm{box-shadow:var(--shadow-sm)}.elev-md{box-shadow:var(--shadow-md)}.elev-lg{box-shadow:var(--shadow-lg)}.tag{letter-spacing:.02em;border-radius:calc(var(--radius-md) * .75);align-items:center;padding:3px 10px;font-size:11px;display:inline-flex}.tag-accent{background:var(--color-accent-800);color:var(--color-accent-100)}.tag-accent-2{background:var(--color-accent-2-800);color:var(--color-accent-2-100)}.tag-neutral{background:var(--color-neutral-800);color:var(--color-neutral-100)}.tag-outline{border:1px solid var(--color-accent);color:var(--color-accent)}.nav{align-items:center;gap:var(--space-4);padding:var(--space-3) var(--space-4);border-bottom:none;display:flex}.nav-brand{font-family:var(--font-heading);font-weight:var(--font-heading-weight);margin-right:auto;font-size:18px}.nav a{color:inherit;font-size:14px;text-decoration:none}.nav a:hover,.nav a[aria-current=page]{color:var(--color-accent)}.table{border-collapse:collapse;width:100%;font-size:14px}.table th{text-align:left;letter-spacing:.08em;text-transform:uppercase;color:color-mix(in srgb, var(--color-text) 60%, transparent);padding:var(--space-2);border-bottom:1px solid #0000;font-size:11px}.table td{padding:var(--space-2);border-bottom:1px solid #0000}.table thead tr{background:linear-gradient(to right, transparent, var(--color-divider) 48px, var(--color-divider) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px}.table tbody tr{background:linear-gradient(to right, transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px, color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px}.table tbody tr:hover{background:linear-gradient(color-mix(in srgb, var(--color-text) 4%, transparent), color-mix(in srgb, var(--color-text) 4%, transparent)) no-repeat 0 0 / 100% 100%, linear-gradient(to right, transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px, color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px}.dialog-backdrop{padding:var(--space-4);background:color-mix(in srgb, var(--color-neutral-900) 50%, transparent);place-items:center;display:grid;position:fixed;inset:0}.dialog{gap:var(--space-3);width:min(440px,100%);padding:var(--space-4);border-radius:var(--radius-lg);background:var(--color-surface);box-shadow:var(--shadow-lg);flex-direction:column;display:flex}.dialog-title{font-family:var(--font-heading);font-weight:var(--font-heading-weight);font-size:20px}.dialog-body{opacity:.85;font-size:14px}.dialog-actions{justify-content:flex-end;gap:var(--space-2);margin-top:var(--space-2);display:flex}:root{--color-bg:#161826;--color-surface:#232532;--color-text:#e9e9ed;--color-accent:#9184d9;--color-divider:#e9e9ed29;--color-neutral-100:#f3f5fe;--color-neutral-200:#e4e7f5;--color-neutral-300:#cfd3e5;--color-neutral-400:#b2b6ca;--color-neutral-500:#9397ab;--color-neutral-600:#75798c;--color-neutral-700:#595d6c;--color-neutral-800:#3f424d;--color-neutral-900:#292b31;--color-accent-100:#f5f4ff;--color-accent-200:#e7e5fe;--color-accent-300:#d2cefd;--color-accent-400:#b5abfc;--color-accent-500:#968ae0;--color-accent-600:#796cbf;--color-accent-700:#5d5294;--color-accent-800:#423a6a;--color-accent-900:#2b2741;--font-heading:"Noto Sans Hebrew", "Inter", system-ui, sans-serif;--font-body:"Noto Sans Hebrew", "Inter", system-ui, sans-serif;--font-hebrew:"Noto Sans Hebrew", "Inter", system-ui, sans-serif;--space-1:2.8px;--space-2:5.6px;--space-3:8.4px;--space-4:11.2px;--space-6:16.8px;--space-8:22.4px;--radius-sm:4px;--radius-md:8px;--radius-lg:14px;--shadow-sm:0 0 0 1px #3f424d;--shadow-md:0 0 0 1px #595d6c, 0 6px 18px #0000008c;--shadow-lg:0 0 0 1px #9397ab, 0 16px 40px #000000a6;--cn-app-bg:#0e0f1a;--cn-board-well:#0e0f18;--cn-board-void:#0b0c13;--cn-panel:#14151f;--cn-surface-2:#1a1c2a;--cn-sheet:#1e2030;--cn-red-ring:#a85454;--cn-red-dot:#c46a6a;--cn-red-text:#ecc9c9;--cn-red-tint:#c46a6a2e;--cn-red-grad:linear-gradient(165deg,#a75050,#7c3a3a);--cn-red-shape:"◆";--cn-blue-ring:#4d7fb3;--cn-blue-dot:#5b8fc7;--cn-blue-text:#cfe2f5;--cn-blue-tint:#5b8fc72e;--cn-blue-grad:linear-gradient(165deg,#3d78b5,#2c5a86);--cn-blue-shape:"●";--cn-neutral-ring:#a08c62;--cn-neutral-dot:#b7a578;--cn-neutral-text:#ded2b5;--cn-neutral-icon:#8a744b;--cn-neutral-tint:#a08c6226;--cn-neutral-grad:linear-gradient(165deg,#d6c493,#bfa96f);--cn-neutral-shape:"−";--cn-assassin-ring:#3c3f4c;--cn-assassin-dot:#aeb2c4;--cn-assassin-icon:#454033;--cn-assassin-tint:#14141e47;--cn-assassin-grad:linear-gradient(165deg,#26272f,#0e0f15);--cn-assassin-shape:"☠";--cn-card-face:linear-gradient(160deg,#f6efdc 0%,#eee3c6 55%,#e0d2ae 100%);--cn-card-label:linear-gradient(180deg,#fffdf6,#f3edd9);--cn-card-label-bd:#c9ba92;--cn-card-ink:#2d2515;--cn-card-mirror:#6554307a;--cn-card-hole:#d8caa2;--cn-warn:#d9bd7a;--cn-warn-bg:#d9bd7a14;--cn-warn-bd:#d9bd7a66;--cn-ok:#7bbf8a;--cn-ok-strong:#5aa06a;--cn-select-ring:#968ae099;--cn-badge-bg:#968ae0;--cn-badge-ink:#161826;--cn-map-bg:#12131e;--cn-map-hint:#968ae0;--cn-map-hint-glow:#968ae059;--cn-map-grid:#1b2436;--cn-text:#e9e9ed;--cn-text-muted:#9397ab;--cn-text-dim:#75798c;--cn-text-faint:#5c6072}html{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:var(--cn-app-bg)}body{background:var(--cn-app-bg);min-width:320px;min-height:100svh;color:var(--cn-text);font-family:var(--font-body)}button,input,textarea,select{font:inherit}button{-webkit-tap-highlight-color:transparent}#root{min-height:100svh}.role-red{--cn-role-ring:var(--cn-red-ring);--cn-role-dot:var(--cn-red-dot);--cn-role-text:var(--cn-red-text);--cn-role-tint:var(--cn-red-tint);--cn-role-gradient:var(--cn-red-grad);--cn-role-shape:var(--cn-red-shape)}.role-blue{--cn-role-ring:var(--cn-blue-ring);--cn-role-dot:var(--cn-blue-dot);--cn-role-text:var(--cn-blue-text);--cn-role-tint:var(--cn-blue-tint);--cn-role-gradient:var(--cn-blue-grad);--cn-role-shape:var(--cn-blue-shape)}.role-neutral{--cn-role-ring:var(--cn-neutral-ring);--cn-role-dot:var(--cn-neutral-dot);--cn-role-text:var(--cn-neutral-text);--cn-role-icon:var(--cn-neutral-icon);--cn-role-tint:var(--cn-neutral-tint);--cn-role-gradient:var(--cn-neutral-grad);--cn-role-shape:var(--cn-neutral-shape)}.role-assassin{--cn-role-ring:var(--cn-assassin-ring);--cn-role-dot:var(--cn-assassin-dot);--cn-role-text:var(--cn-text);--cn-role-icon:var(--cn-assassin-icon);--cn-role-tint:var(--cn-assassin-tint);--cn-role-gradient:var(--cn-assassin-grad);--cn-role-shape:var(--cn-assassin-shape)}.cn-button{min-height:36px;position:relative}.cn-button__content{justify-content:center;align-items:center;gap:var(--space-2);display:inline-flex}.cn-button[data-loading=true] .cn-button__content{visibility:hidden}.cn-loading-spinner{width:var(--space-4);height:var(--space-4);border:2px solid color-mix(in srgb, currentColor 28%, transparent);border-top-color:currentColor;border-radius:50%;animation:.7s linear infinite cn-spin;position:absolute;inset:50% auto auto 50%;transform:translate(-50%,-50%)}@keyframes cn-spin{to{transform:translate(-50%,-50%)rotate(1turn)}}.cn-panel{gap:var(--space-3);min-width:0}.cn-panel__header{justify-content:space-between;align-items:center;gap:var(--space-3);display:flex}.cn-panel__title{color:var(--cn-text);margin:0;font-size:16px;font-weight:600;line-height:1.3}.cn-panel__body{min-width:0}.cn-role-icon{width:1.25em;height:1.25em;color:var(--cn-role-icon,var(--cn-role-dot));font-family:var(--font-body);flex:none;place-items:center;font-size:1em;font-style:normal;font-weight:700;line-height:1;display:inline-grid}.cn-role-icon:before{content:var(--cn-role-shape)}.cn-tooltip{display:inline-flex;position:relative}.cn-tooltip__content{z-index:20;width:max-content;max-width:min(260px, calc(100vw - (2 * var(--space-4))));padding:var(--space-2) var(--space-3);border:1px solid var(--color-divider);border-radius:var(--radius-sm);background:var(--cn-sheet);box-shadow:var(--shadow-md);color:var(--cn-text);opacity:0;pointer-events:none;transform:translate(50%, var(--space-1));font-size:12px;line-height:1.45;transition:opacity .12s,transform .12s;position:absolute;inset-block-end:calc(100% + var(--space-2));inset-inline-start:50%}.cn-tooltip:hover .cn-tooltip__content,.cn-tooltip:focus-within .cn-tooltip__content{opacity:1;transform:translate(50%)}.cn-toast-viewport{z-index:100;width:min(380px, calc(100vw - (2 * var(--space-4))));pointer-events:none;position:fixed;inset-block-end:var(--space-4);inset-inline-start:var(--space-4)}.cn-toast{align-items:flex-start;gap:var(--space-3);padding:var(--space-3) var(--space-4);border:1px solid var(--color-divider);border-inline-start:3px solid var(--cn-toast-accent);border-radius:var(--radius-md);background:var(--cn-sheet);box-shadow:var(--shadow-lg);color:var(--cn-text);pointer-events:auto;animation:.16s ease-out both cn-toast-in;display:flex}.cn-toast--info{--cn-toast-accent:var(--color-accent)}.cn-toast--success{--cn-toast-accent:var(--cn-ok)}.cn-toast--error{--cn-toast-accent:var(--cn-red-dot)}.cn-toast__message{flex:1;margin:0;font-size:13px;line-height:1.5}.cn-toast__close{width:28px;height:28px;margin:calc(var(--space-1) * -1);border-radius:var(--radius-sm);color:var(--cn-text-muted);cursor:pointer;background:0 0;border:0;flex:none}.cn-toast__close:hover{background:color-mix(in srgb, var(--cn-text) 7%, transparent);color:var(--cn-text)}@keyframes cn-toast-in{0%{opacity:0;transform:translateY(var(--space-2))}}@media (prefers-reduced-motion:reduce){.cn-loading-spinner{animation:none}.cn-tooltip__content{transition:none}.cn-toast{animation:none}}
|
webapp/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="he" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="theme-color" content="#0e0f1a" />
|
| 7 |
+
<title>קודנהיימס קופיילוט</title>
|
| 8 |
+
<style>
|
| 9 |
+
html {
|
| 10 |
+
font-family: "Noto Sans Hebrew", "Inter", system-ui, sans-serif;
|
| 11 |
+
}
|
| 12 |
+
</style>
|
| 13 |
+
<script type="module" crossorigin src="/assets/index-LC1qePO9.js"></script>
|
| 14 |
+
<link rel="stylesheet" crossorigin href="/assets/index-w435VGSR.css">
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<div id="root"></div>
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
webapp/mockServiceWorker.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* eslint-disable */
|
| 2 |
+
/* tslint:disable */
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Mock Service Worker.
|
| 6 |
+
* @see https://github.com/mswjs/msw
|
| 7 |
+
* - Please do NOT modify this file.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
const PACKAGE_VERSION = '2.15.0'
|
| 11 |
+
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
|
| 12 |
+
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
| 13 |
+
const activeClientIds = new Set()
|
| 14 |
+
|
| 15 |
+
addEventListener('install', function () {
|
| 16 |
+
self.skipWaiting()
|
| 17 |
+
})
|
| 18 |
+
|
| 19 |
+
addEventListener('activate', function (event) {
|
| 20 |
+
event.waitUntil(self.clients.claim())
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
addEventListener('message', async function (event) {
|
| 24 |
+
const clientId = Reflect.get(event.source || {}, 'id')
|
| 25 |
+
|
| 26 |
+
if (!clientId || !self.clients) {
|
| 27 |
+
return
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const client = await self.clients.get(clientId)
|
| 31 |
+
|
| 32 |
+
if (!client) {
|
| 33 |
+
return
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const allClients = await self.clients.matchAll({
|
| 37 |
+
type: 'window',
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
switch (event.data) {
|
| 41 |
+
case 'KEEPALIVE_REQUEST': {
|
| 42 |
+
sendToClient(client, {
|
| 43 |
+
type: 'KEEPALIVE_RESPONSE',
|
| 44 |
+
})
|
| 45 |
+
break
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
case 'INTEGRITY_CHECK_REQUEST': {
|
| 49 |
+
sendToClient(client, {
|
| 50 |
+
type: 'INTEGRITY_CHECK_RESPONSE',
|
| 51 |
+
payload: {
|
| 52 |
+
packageVersion: PACKAGE_VERSION,
|
| 53 |
+
checksum: INTEGRITY_CHECKSUM,
|
| 54 |
+
},
|
| 55 |
+
})
|
| 56 |
+
break
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
case 'MOCK_ACTIVATE': {
|
| 60 |
+
activeClientIds.add(clientId)
|
| 61 |
+
|
| 62 |
+
sendToClient(client, {
|
| 63 |
+
type: 'MOCKING_ENABLED',
|
| 64 |
+
payload: {
|
| 65 |
+
client: {
|
| 66 |
+
id: client.id,
|
| 67 |
+
frameType: client.frameType,
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
})
|
| 71 |
+
break
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
case 'CLIENT_CLOSED': {
|
| 75 |
+
activeClientIds.delete(clientId)
|
| 76 |
+
|
| 77 |
+
const remainingClients = allClients.filter((client) => {
|
| 78 |
+
return client.id !== clientId
|
| 79 |
+
})
|
| 80 |
+
|
| 81 |
+
// Unregister itself when there are no more clients
|
| 82 |
+
if (remainingClients.length === 0) {
|
| 83 |
+
self.registration.unregister()
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
break
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
addEventListener('fetch', function (event) {
|
| 92 |
+
const requestInterceptedAt = Date.now()
|
| 93 |
+
|
| 94 |
+
// Bypass navigation requests.
|
| 95 |
+
if (event.request.mode === 'navigate') {
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Opening the DevTools triggers the "only-if-cached" request
|
| 100 |
+
// that cannot be handled by the worker. Bypass such requests.
|
| 101 |
+
if (
|
| 102 |
+
event.request.cache === 'only-if-cached' &&
|
| 103 |
+
event.request.mode !== 'same-origin'
|
| 104 |
+
) {
|
| 105 |
+
return
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Bypass all requests when there are no active clients.
|
| 109 |
+
// Prevents the self-unregistered worked from handling requests
|
| 110 |
+
// after it's been terminated (still remains active until the next reload).
|
| 111 |
+
if (activeClientIds.size === 0) {
|
| 112 |
+
return
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const requestId = crypto.randomUUID()
|
| 116 |
+
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
| 117 |
+
})
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* @param {FetchEvent} event
|
| 121 |
+
* @param {string} requestId
|
| 122 |
+
* @param {number} requestInterceptedAt
|
| 123 |
+
*/
|
| 124 |
+
async function handleRequest(event, requestId, requestInterceptedAt) {
|
| 125 |
+
const client = await resolveMainClient(event)
|
| 126 |
+
const requestCloneForEvents = event.request.clone()
|
| 127 |
+
const response = await getResponse(
|
| 128 |
+
event,
|
| 129 |
+
client,
|
| 130 |
+
requestId,
|
| 131 |
+
requestInterceptedAt,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
// Send back the response clone for the "response:*" life-cycle events.
|
| 135 |
+
// Ensure MSW is active and ready to handle the message, otherwise
|
| 136 |
+
// this message will pend indefinitely.
|
| 137 |
+
if (client && activeClientIds.has(client.id)) {
|
| 138 |
+
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
| 139 |
+
|
| 140 |
+
// Omit the body of server-sent event stream responses.
|
| 141 |
+
// Cloning such responses would prevent client-side stream cancelations
|
| 142 |
+
// from reaching the original stream (a teed stream only cancels its
|
| 143 |
+
// source once both of its branches cancel) and would buffer the
|
| 144 |
+
// entire stream into the unconsumed clone indefinitely.
|
| 145 |
+
const isEventStreamResponse = response.headers
|
| 146 |
+
.get('content-type')
|
| 147 |
+
?.toLowerCase()
|
| 148 |
+
.startsWith('text/event-stream')
|
| 149 |
+
|
| 150 |
+
// Clone the response so both the client and the library could consume it.
|
| 151 |
+
const responseClone = isEventStreamResponse ? null : response.clone()
|
| 152 |
+
|
| 153 |
+
sendToClient(
|
| 154 |
+
client,
|
| 155 |
+
{
|
| 156 |
+
type: 'RESPONSE',
|
| 157 |
+
payload: {
|
| 158 |
+
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
| 159 |
+
request: {
|
| 160 |
+
id: requestId,
|
| 161 |
+
...serializedRequest,
|
| 162 |
+
},
|
| 163 |
+
response: {
|
| 164 |
+
type: response.type,
|
| 165 |
+
status: response.status,
|
| 166 |
+
statusText: response.statusText,
|
| 167 |
+
headers: Object.fromEntries(response.headers.entries()),
|
| 168 |
+
body: responseClone ? responseClone.body : null,
|
| 169 |
+
},
|
| 170 |
+
},
|
| 171 |
+
},
|
| 172 |
+
responseClone && responseClone.body
|
| 173 |
+
? [serializedRequest.body, responseClone.body]
|
| 174 |
+
: [],
|
| 175 |
+
)
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
return response
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* Resolve the main client for the given event.
|
| 183 |
+
* Client that issues a request doesn't necessarily equal the client
|
| 184 |
+
* that registered the worker. It's with the latter the worker should
|
| 185 |
+
* communicate with during the response resolving phase.
|
| 186 |
+
* @param {FetchEvent} event
|
| 187 |
+
* @returns {Promise<Client | undefined>}
|
| 188 |
+
*/
|
| 189 |
+
async function resolveMainClient(event) {
|
| 190 |
+
const client = await self.clients.get(event.clientId)
|
| 191 |
+
|
| 192 |
+
if (activeClientIds.has(event.clientId)) {
|
| 193 |
+
return client
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
if (client?.frameType === 'top-level') {
|
| 197 |
+
return client
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
const allClients = await self.clients.matchAll({
|
| 201 |
+
type: 'window',
|
| 202 |
+
})
|
| 203 |
+
|
| 204 |
+
return allClients
|
| 205 |
+
.filter((client) => {
|
| 206 |
+
// Get only those clients that are currently visible.
|
| 207 |
+
return client.visibilityState === 'visible'
|
| 208 |
+
})
|
| 209 |
+
.find((client) => {
|
| 210 |
+
// Find the client ID that's recorded in the
|
| 211 |
+
// set of clients that have registered the worker.
|
| 212 |
+
return activeClientIds.has(client.id)
|
| 213 |
+
})
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/**
|
| 217 |
+
* @param {FetchEvent} event
|
| 218 |
+
* @param {Client | undefined} client
|
| 219 |
+
* @param {string} requestId
|
| 220 |
+
* @param {number} requestInterceptedAt
|
| 221 |
+
* @returns {Promise<Response>}
|
| 222 |
+
*/
|
| 223 |
+
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
| 224 |
+
// Clone the request because it might've been already used
|
| 225 |
+
// (i.e. its body has been read and sent to the client).
|
| 226 |
+
const requestClone = event.request.clone()
|
| 227 |
+
|
| 228 |
+
function passthrough() {
|
| 229 |
+
// Cast the request headers to a new Headers instance
|
| 230 |
+
// so the headers can be manipulated with.
|
| 231 |
+
const headers = new Headers(requestClone.headers)
|
| 232 |
+
|
| 233 |
+
// Remove the "accept" header value that marked this request as passthrough.
|
| 234 |
+
// This prevents request alteration and also keeps it compliant with the
|
| 235 |
+
// user-defined CORS policies.
|
| 236 |
+
const acceptHeader = headers.get('accept')
|
| 237 |
+
if (acceptHeader) {
|
| 238 |
+
const values = acceptHeader.split(',').map((value) => value.trim())
|
| 239 |
+
const filteredValues = values.filter(
|
| 240 |
+
(value) => value !== 'msw/passthrough',
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
if (filteredValues.length > 0) {
|
| 244 |
+
headers.set('accept', filteredValues.join(', '))
|
| 245 |
+
} else {
|
| 246 |
+
headers.delete('accept')
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
return fetch(requestClone, { headers })
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// Bypass mocking when the client is not active.
|
| 254 |
+
if (!client) {
|
| 255 |
+
return passthrough()
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// Bypass initial page load requests (i.e. static assets).
|
| 259 |
+
// The absence of the immediate/parent client in the map of the active clients
|
| 260 |
+
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
| 261 |
+
// and is not ready to handle requests.
|
| 262 |
+
if (!activeClientIds.has(client.id)) {
|
| 263 |
+
return passthrough()
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
// Notify the client that a request has been intercepted.
|
| 267 |
+
const serializedRequest = await serializeRequest(event.request)
|
| 268 |
+
const clientMessage = await sendToClient(
|
| 269 |
+
client,
|
| 270 |
+
{
|
| 271 |
+
type: 'REQUEST',
|
| 272 |
+
payload: {
|
| 273 |
+
id: requestId,
|
| 274 |
+
interceptedAt: requestInterceptedAt,
|
| 275 |
+
...serializedRequest,
|
| 276 |
+
},
|
| 277 |
+
},
|
| 278 |
+
[serializedRequest.body],
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
switch (clientMessage.type) {
|
| 282 |
+
case 'MOCK_RESPONSE': {
|
| 283 |
+
return respondWithMock(clientMessage.data)
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
case 'PASSTHROUGH': {
|
| 287 |
+
return passthrough()
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
return passthrough()
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/**
|
| 295 |
+
* @param {Client} client
|
| 296 |
+
* @param {any} message
|
| 297 |
+
* @param {Array<Transferable>} transferrables
|
| 298 |
+
* @returns {Promise<any>}
|
| 299 |
+
*/
|
| 300 |
+
function sendToClient(client, message, transferrables = []) {
|
| 301 |
+
return new Promise((resolve, reject) => {
|
| 302 |
+
const channel = new MessageChannel()
|
| 303 |
+
|
| 304 |
+
channel.port1.onmessage = (event) => {
|
| 305 |
+
if (event.data && event.data.error) {
|
| 306 |
+
return reject(event.data.error)
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
resolve(event.data)
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
client.postMessage(message, [
|
| 313 |
+
channel.port2,
|
| 314 |
+
...transferrables.filter(Boolean),
|
| 315 |
+
])
|
| 316 |
+
})
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
/**
|
| 320 |
+
* @param {Response} response
|
| 321 |
+
* @returns {Response}
|
| 322 |
+
*/
|
| 323 |
+
function respondWithMock(response) {
|
| 324 |
+
// Setting response status code to 0 is a no-op.
|
| 325 |
+
// However, when responding with a "Response.error()", the produced Response
|
| 326 |
+
// instance will have status code set to 0. Since it's not possible to create
|
| 327 |
+
// a Response instance with status code 0, handle that use-case separately.
|
| 328 |
+
if (response.status === 0) {
|
| 329 |
+
return Response.error()
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
const mockedResponse = new Response(response.body, response)
|
| 333 |
+
|
| 334 |
+
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
| 335 |
+
value: true,
|
| 336 |
+
enumerable: true,
|
| 337 |
+
})
|
| 338 |
+
|
| 339 |
+
return mockedResponse
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/**
|
| 343 |
+
* @param {Request} request
|
| 344 |
+
*/
|
| 345 |
+
async function serializeRequest(request) {
|
| 346 |
+
return {
|
| 347 |
+
url: request.url,
|
| 348 |
+
mode: request.mode,
|
| 349 |
+
method: request.method,
|
| 350 |
+
headers: Object.fromEntries(request.headers.entries()),
|
| 351 |
+
cache: request.cache,
|
| 352 |
+
credentials: request.credentials,
|
| 353 |
+
destination: request.destination,
|
| 354 |
+
integrity: request.integrity,
|
| 355 |
+
redirect: request.redirect,
|
| 356 |
+
referrer: request.referrer,
|
| 357 |
+
referrerPolicy: request.referrerPolicy,
|
| 358 |
+
body: await request.arrayBuffer(),
|
| 359 |
+
keepalive: request.keepalive,
|
| 360 |
+
}
|
| 361 |
+
}
|