Files
redl-gamepanel/resources/justrp/html/app.html
T
redlandClaude Fable 5 4cc10b264b Opus 5 redesign: county-records theme across all NUI screens
Auth becomes RESIDENT INTAKE (form LS-01), character creation an
identity-record intake with a live paper card (LS-02), spawn select a
county survey grid with five entry points (LS-03). Live validation,
click-drag character rotation, animated transitions throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:43:17 +00:00

1204 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Los Santos County — Resident Intake</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--ink: #10131A;
--ink-2: #171B24;
--ink-3: #1E2430;
--paper: #E8E4DA;
--paper-2: #D6D0C2;
--paper-3: #C3BCAA;
--stamp: #C2462E;
--stamp-d: #9E3623;
--rule: rgba(232,228,218,.13);
--rule-2: rgba(232,228,218,.06);
--dim: rgba(232,228,218,.44);
--dim-2: rgba(232,228,218,.24);
--ink-rule:rgba(16,19,26,.16);
--ink-dim: rgba(16,19,26,.52);
--sig: 'Barlow Condensed','Oswald',sans-serif;
--body: 'IBM Plex Sans',system-ui,sans-serif;
--mono: 'IBM Plex Mono',ui-monospace,monospace;
--e: cubic-bezier(.2,.8,.25,1);
}
html,body{width:100%;height:100%;overflow:hidden;background:transparent;
color:var(--paper);font-family:var(--body);-webkit-font-smoothing:antialiased}
.screen{position:fixed;inset:0;opacity:0;pointer-events:none;
transition:opacity .45s var(--e);display:flex}
.screen.on{opacity:1;pointer-events:auto}
/* ══ Shared: file spine ═══════════════════════════ */
.spine{width:58px;flex-shrink:0;background:var(--ink);border-right:1px solid var(--rule);
display:flex;flex-direction:column;align-items:center;justify-content:space-between;
padding:22px 0;position:relative}
.spine-txt{writing-mode:vertical-rl;font-family:var(--mono);font-size:10px;
letter-spacing:.28em;color:var(--dim-2);white-space:nowrap}
.spine-mark{width:14px;height:14px;border:1px solid var(--stamp);position:relative}
.spine-mark::after{content:'';position:absolute;inset:3px;background:var(--stamp);
animation:breathe 3.4s var(--e) infinite}
@keyframes breathe{0%,100%{opacity:.25}50%{opacity:1}}
/* ══ Shared: buttons ══════════════════════════════ */
.btn{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;
padding:13px 22px;border:1px solid transparent;cursor:pointer;background:none;
color:var(--paper);position:relative;overflow:hidden;
transition:transform .12s var(--e),background .22s var(--e),
color .22s var(--e),border-color .22s var(--e),opacity .2s}
.btn:disabled{opacity:.32;cursor:not-allowed}
.btn:focus-visible{outline:1px solid var(--stamp);outline-offset:3px}
.btn-fill{background:var(--stamp);color:var(--paper);border-color:var(--stamp)}
.btn-fill:not(:disabled):hover{background:var(--stamp-d);border-color:var(--stamp-d)}
.btn-fill:not(:disabled):active{transform:translateY(1px)}
.btn-line{border-color:var(--rule);color:var(--dim)}
.btn-line:not(:disabled):hover{border-color:var(--paper);color:var(--paper)}
.btn-line:not(:disabled):active{transform:translateY(1px)}
/* sweep on press */
.btn-fill::after{content:'';position:absolute;inset:0;background:var(--paper);
opacity:0;transition:opacity .3s var(--e)}
.btn-fill:not(:disabled):active::after{opacity:.22;transition:none}
/* ══ Shared: fields ═══════════════════════════════ */
.fld{margin-bottom:17px}
.fld-lb{display:flex;justify-content:space-between;align-items:baseline;
font-family:var(--mono);font-size:9.5px;letter-spacing:.19em;text-transform:uppercase;
color:var(--dim-2);margin-bottom:7px;transition:color .2s var(--e)}
.fld:focus-within .fld-lb{color:var(--stamp)}
.fld-lb i{font-style:normal;color:var(--dim-2)}
.fld input,.fld textarea{width:100%;background:rgba(232,228,218,.035);
border:1px solid var(--rule);border-radius:0;padding:11px 13px;
font-family:var(--body);font-size:14px;color:var(--paper);outline:none;resize:none;
transition:border-color .2s var(--e),background .2s var(--e)}
.fld input::placeholder,.fld textarea::placeholder{color:rgba(232,228,218,.19)}
.fld input:focus,.fld textarea:focus{border-color:var(--paper);background:rgba(232,228,218,.06)}
.fld input.bad,.fld textarea.bad{border-color:var(--stamp)}
.fld-msg{font-family:var(--mono);font-size:10px;letter-spacing:.06em;color:var(--stamp);
margin-top:6px;min-height:14px;opacity:0;transform:translateY(-3px);
transition:opacity .22s var(--e),transform .22s var(--e)}
.fld-msg.on{opacity:1;transform:none}
.pw-box{position:relative}
.pw-eye{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;
border:none;cursor:pointer;color:var(--dim-2);font-family:var(--mono);font-size:9px;
letter-spacing:.12em;padding:4px 6px;transition:color .2s var(--e)}
.pw-eye:hover{color:var(--paper)}
.pw-eye:focus-visible{outline:1px solid var(--stamp)}
/* strength — accent opacity steps, no second hue */
.pw-str{display:flex;gap:3px;margin-top:7px;height:2px}
.pw-str i{flex:1;background:var(--rule);transition:background .3s var(--e)}
/* ══ 1. AUTH ══════════════════════════════════════ */
#sc-auth{background:var(--ink)}
.au-main{flex:1;display:flex;align-items:center;justify-content:center;padding:40px 56px;
position:relative;overflow:hidden}
.au-main::before{content:'';position:absolute;inset:0;pointer-events:none;
background:radial-gradient(ellipse 52% 62% at 78% 42%,rgba(194,70,46,.07),transparent 70%)}
.au-wrap{width:100%;max-width:920px;display:grid;grid-template-columns:1fr 372px;
gap:64px;align-items:center;position:relative}
.au-lede{max-width:420px}
.au-eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.3em;color:var(--stamp);
margin-bottom:15px}
.au-h1{font-family:var(--sig);font-weight:700;font-size:clamp(46px,5.6vw,78px);line-height:.86;
text-transform:uppercase;letter-spacing:-.012em;margin-bottom:22px}
.au-h1 em{font-style:normal;color:var(--stamp);display:block}
.au-rule{height:1px;background:var(--rule);margin-bottom:20px}
.au-p{font-size:13.5px;line-height:1.8;color:var(--dim)}
.au-meta{margin-top:30px;display:flex;gap:30px}
.au-meta-i{font-family:var(--mono);font-size:10px;letter-spacing:.13em;color:var(--dim-2);line-height:1.9}
.au-meta-i b{display:block;color:var(--paper);font-size:19px;font-weight:500;letter-spacing:0}
/* the form is a paper document on the ink ground */
.au-doc{background:var(--paper);color:var(--ink);padding:30px 30px 26px;position:relative;
box-shadow:0 26px 60px rgba(0,0,0,.5);
animation:docIn .7s var(--e) both}
@keyframes docIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}
.au-doc::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--stamp)}
.au-doc-hd{display:flex;justify-content:space-between;align-items:baseline;
padding-bottom:13px;border-bottom:1px solid var(--ink-rule);margin-bottom:20px}
.au-doc-ti{font-family:var(--sig);font-size:23px;font-weight:700;text-transform:uppercase;
letter-spacing:.01em;line-height:1}
.au-doc-no{font-family:var(--mono);font-size:9.5px;letter-spacing:.15em;color:var(--ink-dim)}
/* toggle: two states, one screen */
.au-tabs{display:flex;border:1px solid var(--ink-rule);margin-bottom:20px;position:relative}
.au-tabs::before{content:'';position:absolute;top:0;bottom:0;left:0;width:50%;
background:var(--ink);transition:transform .38s var(--e)}
.au-tabs[data-m="reg"]::before{transform:translateX(100%)}
.au-tab{flex:1;padding:9px 0;text-align:center;cursor:pointer;position:relative;z-index:1;
background:none;border:none;
font-family:var(--mono);font-size:10px;letter-spacing:.17em;text-transform:uppercase;
color:var(--ink-dim);transition:color .3s var(--e);user-select:none}
.au-tab.on{color:var(--paper)}
.au-tab:focus-visible{outline:1px solid var(--stamp);outline-offset:-3px}
/* paper-context field overrides — scoped, no bleed */
.au-doc .fld-lb{color:var(--ink-dim)}
.au-doc .fld:focus-within .fld-lb{color:var(--stamp)}
.au-doc .fld input{background:rgba(16,19,26,.04);border-color:var(--ink-rule);color:var(--ink)}
.au-doc .fld input::placeholder{color:rgba(16,19,26,.3)}
.au-doc .fld input:focus{border-color:var(--ink);background:rgba(16,19,26,.06)}
.au-doc .fld input.bad{border-color:var(--stamp)}
.au-doc .pw-eye{color:var(--ink-dim)}
.au-doc .pw-eye:hover{color:var(--ink)}
.au-doc .pw-str i{background:var(--ink-rule)}
.au-pane{display:none}
.au-pane.on{display:block;animation:paneIn .34s var(--e)}
@keyframes paneIn{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
.au-alert{background:rgba(194,70,46,.09);border-left:2px solid var(--stamp);
padding:0 13px;margin-bottom:0;font-size:12.5px;line-height:1.55;color:var(--stamp-d);
max-height:0;overflow:hidden;opacity:0;
transition:max-height .3s var(--e),opacity .3s var(--e),padding .3s var(--e),margin .3s var(--e)}
.au-alert.on{max-height:90px;opacity:1;padding:11px 13px;margin-bottom:17px}
.au-foot{margin-top:19px;padding-top:15px;border-top:1px solid var(--ink-rule);
font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--ink-dim);
display:flex;justify-content:space-between;align-items:center}
.au-sw{color:var(--stamp);cursor:pointer;text-decoration:none;border-bottom:1px solid transparent;
transition:border-color .2s var(--e)}
.au-sw:hover{border-bottom-color:var(--stamp)}
/* ══ 2. RECORDS DRAWER ════════════════════════════ */
#sc-chars{background:var(--ink)}
.cs-bar{display:flex;justify-content:space-between;align-items:center;
padding:20px 40px;border-bottom:1px solid var(--rule)}
.cs-bar-l{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--dim-2)}
.cs-bar-l b{color:var(--paper);font-weight:500}
.cs-body{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
padding:40px}
.cs-hd{text-align:center;margin-bottom:38px;animation:riseIn .55s var(--e) both}
.cs-hd h2{font-family:var(--sig);font-weight:700;font-size:44px;text-transform:uppercase;
letter-spacing:-.008em;line-height:1;margin-bottom:9px}
.cs-hd p{font-family:var(--mono);font-size:10.5px;letter-spacing:.17em;color:var(--dim-2)}
@keyframes riseIn{from{opacity:0;transform:translateY(13px)}to{opacity:1;transform:none}}
.cs-grid{display:flex;gap:19px;flex-wrap:wrap;justify-content:center}
.cs-card{width:236px;background:var(--paper);color:var(--ink);cursor:pointer;
position:relative;text-align:left;border:none;font-family:inherit;
animation:riseIn .55s var(--e) both;
transition:transform .26s var(--e),box-shadow .26s var(--e)}
.cs-card:nth-child(2){animation-delay:.07s}
.cs-card:nth-child(3){animation-delay:.14s}
.cs-card:hover{transform:translateY(-6px);box-shadow:0 20px 44px rgba(0,0,0,.5)}
.cs-card:active{transform:translateY(-2px)}
.cs-card:focus-visible{outline:2px solid var(--stamp);outline-offset:3px}
.cs-card-top{height:3px;background:var(--stamp)}
.cs-card-in{padding:19px}
.cs-card-no{font-family:var(--mono);font-size:9px;letter-spacing:.16em;color:var(--ink-dim);
margin-bottom:14px}
.cs-card-nm{font-family:var(--sig);font-size:27px;font-weight:700;text-transform:uppercase;
line-height:1.02;margin-bottom:3px;word-break:break-word}
.cs-card-sub{font-family:var(--mono);font-size:9.5px;letter-spacing:.13em;color:var(--ink-dim);
padding-bottom:14px;border-bottom:1px solid var(--ink-rule);margin-bottom:13px}
.cs-tbl{display:grid;grid-template-columns:auto 1fr;gap:5px 12px;
font-family:var(--mono);font-size:9.5px;letter-spacing:.06em}
.cs-tbl dt{color:var(--ink-dim)}
.cs-tbl dd{text-align:right;font-weight:500}
/* empty slot: a blank form, not a plus sign */
.cs-card.blank{background:transparent;border:1px dashed var(--rule);color:var(--paper)}
.cs-card.blank:hover{border-color:var(--stamp);border-style:solid;box-shadow:none}
.cs-card.blank .cs-card-top{background:transparent}
.cs-blank-in{padding:34px 19px;display:flex;flex-direction:column;
align-items:flex-start;justify-content:center;min-height:186px}
.cs-blank-no{font-family:var(--mono);font-size:9px;letter-spacing:.16em;color:var(--dim-2);
margin-bottom:12px}
.cs-blank-t{font-family:var(--sig);font-size:25px;font-weight:600;text-transform:uppercase;
color:var(--dim);line-height:1.05;transition:color .24s var(--e)}
.cs-card.blank:hover .cs-blank-t{color:var(--paper)}
.cs-blank-d{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;color:var(--dim-2);
margin-top:9px}
/* ══ 3. INTAKE / CHARACTER CREATION ═══════════════ */
#sc-create{background:transparent}
/* left: live ped, transparent to the game */
.cc-stage{flex:1;position:relative;cursor:grab;user-select:none;
background:linear-gradient(90deg,rgba(16,19,26,.82) 0%,rgba(16,19,26,.18) 42%,transparent 100%)}
.cc-stage:active{cursor:grabbing}
.cc-stage-hd{position:absolute;top:26px;left:34px;font-family:var(--mono);font-size:10px;
letter-spacing:.2em;color:var(--dim);line-height:2}
.cc-stage-hd b{color:var(--paper);font-weight:500}
.cc-drag{position:absolute;bottom:30px;left:34px;font-family:var(--mono);font-size:9.5px;
letter-spacing:.16em;color:var(--dim-2);display:flex;align-items:center;gap:9px}
.cc-drag i{display:block;width:22px;height:1px;background:var(--stamp);
animation:swipe 2.3s var(--e) infinite}
@keyframes swipe{0%,100%{transform:translateX(0);opacity:.3}50%{transform:translateX(9px);opacity:1}}
/* ── SIGNATURE: the record card assembling live ── */
.cc-id{position:absolute;left:34px;top:50%;transform:translateY(-50%);width:262px;
background:var(--paper);color:var(--ink);box-shadow:0 22px 54px rgba(0,0,0,.58);
animation:docIn .6s var(--e) both}
.cc-id-top{height:3px;background:var(--stamp)}
.cc-id-in{padding:16px 17px 17px}
.cc-id-hd{display:flex;justify-content:space-between;align-items:flex-start;
padding-bottom:11px;border-bottom:1px solid var(--ink-rule);margin-bottom:12px}
.cc-id-au{font-family:var(--mono);font-size:8px;letter-spacing:.15em;color:var(--ink-dim);line-height:1.75}
.cc-id-au b{color:var(--ink);font-weight:600}
.cc-id-no{font-family:var(--mono);font-size:8px;letter-spacing:.1em;color:var(--stamp);text-align:right}
.cc-id-nm{font-family:var(--sig);font-size:29px;font-weight:700;text-transform:uppercase;
line-height:.98;min-height:29px;margin-bottom:2px;word-break:break-word;
transition:color .3s var(--e)}
.cc-id-nm.pend{color:var(--ink-rule)}
.cc-id-role{font-family:var(--mono);font-size:8.5px;letter-spacing:.14em;color:var(--ink-dim);
padding-bottom:12px;border-bottom:1px solid var(--ink-rule);margin-bottom:12px}
.cc-id-tbl{display:grid;grid-template-columns:auto 1fr;gap:6px 10px;
font-family:var(--mono);font-size:9px;letter-spacing:.05em}
.cc-id-tbl dt{color:var(--ink-dim)}
.cc-id-tbl dd{text-align:right;font-weight:500;transition:color .25s var(--e)}
.cc-id-tbl dd.hit{color:var(--stamp)}
.cc-id-bar{margin-top:13px;padding-top:11px;border-top:1px solid var(--ink-rule);
display:flex;gap:1.5px;align-items:flex-end;height:24px}
.cc-id-bar i{flex:1;background:var(--ink);opacity:.7}
/* the stamp */
.cc-stampmark{position:absolute;right:-14px;bottom:34px;
font-family:var(--sig);font-size:31px;font-weight:700;letter-spacing:.09em;
text-transform:uppercase;color:var(--stamp);border:3px solid var(--stamp);
padding:5px 13px;transform:rotate(-11deg) scale(2.6);opacity:0;pointer-events:none}
.cc-stampmark.hit{animation:stampHit .5s cubic-bezier(.3,1.5,.5,1) forwards}
@keyframes stampHit{
0%{opacity:0;transform:rotate(-11deg) scale(2.6)}
55%{opacity:.95;transform:rotate(-11deg) scale(.93)}
75%{transform:rotate(-11deg) scale(1.05)}
100%{opacity:.9;transform:rotate(-11deg) scale(1)}}
/* right: the form */
.cc-panel{width:392px;flex-shrink:0;background:var(--ink);border-left:1px solid var(--rule);
display:flex;flex-direction:column}
.cc-hd{padding:22px 28px 17px;border-bottom:1px solid var(--rule);flex-shrink:0}
.cc-hd-no{font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--stamp);margin-bottom:8px}
.cc-hd h2{font-family:var(--sig);font-weight:700;font-size:29px;text-transform:uppercase;
line-height:1;margin-bottom:4px}
.cc-hd p{font-family:var(--mono);font-size:9.5px;letter-spacing:.13em;color:var(--dim-2)}
/* section markers: real form sections, so numbering is earned */
.cc-secs{display:flex;gap:0;margin-top:15px;border-top:1px solid var(--rule-2)}
.cc-sec-t{flex:1;padding:9px 0 0;font-family:var(--mono);font-size:8.5px;letter-spacing:.13em;
color:var(--dim-2);border-top:2px solid transparent;margin-top:-1px;
transition:color .3s var(--e),border-color .3s var(--e)}
.cc-sec-t.on{color:var(--paper);border-top-color:var(--stamp)}
.cc-sec-t.done{color:var(--dim)}
.cc-body{flex:1;overflow-y:auto;padding:22px 28px;scrollbar-width:thin;
scrollbar-color:var(--rule) transparent}
.cc-body::-webkit-scrollbar{width:3px}
.cc-body::-webkit-scrollbar-thumb{background:var(--rule)}
.cc-pane{display:none}
.cc-pane.on{display:block;animation:paneIn .32s var(--e)}
.cc-gl{font-family:var(--mono);font-size:9px;letter-spacing:.2em;color:var(--stamp);
padding-bottom:9px;border-bottom:1px solid var(--rule);margin-bottom:16px}
.cc-gl:not(:first-child){margin-top:26px}
/* sliders */
.sl{margin-bottom:16px}
.sl-lb{display:flex;justify-content:space-between;align-items:baseline;
font-family:var(--mono);font-size:10px;letter-spacing:.1em;color:var(--dim);margin-bottom:8px}
.sl-lb b{color:var(--paper);font-weight:500;font-variant-numeric:tabular-nums}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:1px;
background:var(--rule);outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
width:11px;height:11px;background:var(--paper);cursor:pointer;
transition:transform .16s var(--e),background .16s var(--e)}
input[type=range]:hover::-webkit-slider-thumb{transform:scale(1.4);background:var(--stamp)}
input[type=range]:focus-visible{outline:1px solid var(--stamp);outline-offset:5px}
/* swatch + option grids */
.sw-grid{display:flex;flex-wrap:wrap;gap:5px}
.sw{width:25px;height:25px;border:1px solid var(--rule);cursor:pointer;padding:0;
transition:transform .16s var(--e),border-color .16s var(--e)}
.sw:hover{transform:scale(1.13);border-color:var(--paper)}
.sw.on{border-color:var(--stamp);box-shadow:inset 0 0 0 1px var(--ink)}
.sw:focus-visible{outline:1px solid var(--stamp);outline-offset:2px}
.op-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:4px}
.op{aspect-ratio:1;background:rgba(232,228,218,.035);border:1px solid var(--rule);
cursor:pointer;font-family:var(--mono);font-size:9.5px;color:var(--dim-2);padding:0;
transition:border-color .16s var(--e),color .16s var(--e),background .16s var(--e)}
.op:hover{border-color:var(--paper);color:var(--paper)}
.op.on{border-color:var(--stamp);color:var(--stamp);background:rgba(194,70,46,.11)}
.op:focus-visible{outline:1px solid var(--stamp);outline-offset:2px}
/* two-state pills */
.pill-row{display:flex;gap:7px;margin-bottom:19px}
.pill{flex:1;padding:11px 0;border:1px solid var(--rule);cursor:pointer;background:none;
font-family:var(--mono);font-size:10px;letter-spacing:.15em;text-transform:uppercase;
color:var(--dim);transition:all .2s var(--e)}
.pill:hover{border-color:var(--paper);color:var(--paper)}
.pill.on{border-color:var(--stamp);background:rgba(194,70,46,.11);color:var(--stamp)}
.pill:focus-visible{outline:1px solid var(--stamp);outline-offset:2px}
.cc-ft{padding:16px 28px;border-top:1px solid var(--rule);display:flex;gap:9px;flex-shrink:0}
.cc-ft .btn{flex:1}
.cc-ft .btn-line{flex:0 0 auto}
.cc-err{padding:0 28px 13px;font-family:var(--mono);font-size:10px;letter-spacing:.06em;
color:var(--stamp);min-height:0;text-align:center}
/* ══ 4. ENTRY MAP ═════════════════════════════════ */
#sc-spawn{background:var(--ink)}
.sp-bar{display:flex;justify-content:space-between;align-items:center;padding:20px 40px;
border-bottom:1px solid var(--rule);flex-shrink:0}
.sp-bar-l{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--dim-2)}
.sp-bar-l b{color:var(--paper);font-weight:500}
.sp-body{flex:1;display:flex;min-height:0}
.sp-mapwrap{flex:1;position:relative;padding:34px;display:flex;
align-items:center;justify-content:center}
.sp-map{position:relative;height:min(100%,680px);width:auto;aspect-ratio:3/4;
max-width:100%;flex-shrink:0;
border:1px solid var(--rule);background:
linear-gradient(rgba(232,228,218,.028) 1px,transparent 1px) 0 0/100% 9.09%,
linear-gradient(90deg,rgba(232,228,218,.028) 1px,transparent 1px) 0 0/9.09% 100%,
radial-gradient(ellipse 62% 52% at 42% 76%,rgba(194,70,46,.06),transparent 70%);
animation:docIn .6s var(--e) both}
/* coastline: the county's actual silhouette, abstracted */
.sp-coast{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.sp-coast path{fill:rgba(232,228,218,.032);stroke:var(--rule);stroke-width:1;
vector-effect:non-scaling-stroke}
.sp-pin{position:absolute;transform:translate(-50%,-50%);cursor:pointer;background:none;
border:none;padding:0;width:15px;height:15px}
.sp-pin::before{content:'';position:absolute;inset:4px;background:var(--dim);
transition:background .22s var(--e),transform .22s var(--e)}
.sp-pin::after{content:'';position:absolute;inset:0;border:1px solid transparent;
border-radius:50%;transition:border-color .22s var(--e),transform .35s var(--e)}
.sp-pin:hover::before{background:var(--paper);transform:scale(1.25)}
.sp-pin:hover::after{border-color:var(--rule)}
.sp-pin.on::before{background:var(--stamp);transform:scale(1.3)}
.sp-pin.on::after{border-color:var(--stamp);animation:ping 1.9s var(--e) infinite}
.sp-pin:focus-visible{outline:1px solid var(--stamp);outline-offset:4px}
@keyframes ping{0%{transform:scale(1);opacity:1}70%{transform:scale(2.5);opacity:0}100%{opacity:0}}
.sp-pin-lb{position:absolute;left:20px;top:50%;transform:translateY(-50%);
font-family:var(--mono);font-size:9px;letter-spacing:.14em;white-space:nowrap;
color:var(--dim-2);transition:color .22s var(--e)}
.sp-pin:hover .sp-pin-lb{color:var(--paper)}
.sp-pin.on .sp-pin-lb{color:var(--stamp)}
.sp-pin[data-flip="1"] .sp-pin-lb{left:auto;right:20px}
/* detail dossier */
.sp-side{width:330px;flex-shrink:0;border-left:1px solid var(--rule);
display:flex;flex-direction:column;padding:34px 30px}
.sp-empty{margin:auto 0;font-family:var(--mono);font-size:10.5px;letter-spacing:.15em;
color:var(--dim-2);line-height:2}
.sp-det{display:none}
.sp-det.on{display:block;animation:paneIn .34s var(--e)}
.sp-det-no{font-family:var(--mono);font-size:9.5px;letter-spacing:.19em;color:var(--stamp);
margin-bottom:11px}
.sp-det-nm{font-family:var(--sig);font-weight:700;font-size:40px;text-transform:uppercase;
line-height:.94;margin-bottom:14px}
.sp-det-p{font-size:13px;line-height:1.8;color:var(--dim);
padding-bottom:18px;border-bottom:1px solid var(--rule);margin-bottom:18px}
.sp-det-tbl{display:grid;grid-template-columns:auto 1fr;gap:9px 14px;
font-family:var(--mono);font-size:10px;letter-spacing:.07em;margin-bottom:26px}
.sp-det-tbl dt{color:var(--dim-2)}
.sp-det-tbl dd{text-align:right;color:var(--paper)}
.sp-go{margin-top:auto}
@media (prefers-reduced-motion:reduce){
*{animation-duration:.01ms!important;animation-iteration-count:1!important;
transition-duration:.01ms!important}
}
@media (max-width:1180px){
.au-wrap{grid-template-columns:1fr;gap:34px;max-width:420px}
.au-lede{display:none}
.cc-id{display:none}
.sp-side{width:280px}
}
@media (max-width:820px){
.spine{display:none}
.cc-panel{width:330px}
.sp-body{flex-direction:column}
.sp-side{width:auto;border-left:none;border-top:1px solid var(--rule);padding:22px}
}
</style>
</head>
<body>
<!-- ══ 1. AUTH ══ -->
<div id="sc-auth" class="screen">
<div class="spine">
<div class="spine-txt">FORM LS-01 · RESIDENCY</div>
<div class="spine-mark"></div>
<div class="spine-txt">DEPT. RESIDENT AFFAIRS</div>
</div>
<div class="au-main">
<div class="au-wrap">
<div class="au-lede">
<div class="au-eyebrow">LOS SANTOS COUNTY</div>
<h1 class="au-h1">Resident<em>Intake</em></h1>
<div class="au-rule"></div>
<p class="au-p">Everyone in this county is on file. Open yours, or sign back into the one you already have — the city picks up exactly where you left it.</p>
<div class="au-meta">
<div class="au-meta-i"><b>64</b>RESIDENT CAP</div>
<div class="au-meta-i"><b>3</b>RECORDS PER FILE</div>
<div class="au-meta-i"><b>24/7</b>COUNTY UPTIME</div>
</div>
</div>
<div class="au-doc">
<div class="au-doc-hd">
<div class="au-doc-ti" id="au-ti">Sign In</div>
<div class="au-doc-no">LS-01</div>
</div>
<div class="au-tabs" id="au-tabs" data-m="log">
<button class="au-tab on" data-m="log" type="button">Existing File</button>
<button class="au-tab" data-m="reg" type="button">Open a File</button>
</div>
<div class="au-alert" id="au-alert" role="alert"></div>
<!-- login -->
<div class="au-pane on" id="pane-log">
<div class="fld">
<div class="fld-lb"><span>Username</span></div>
<input id="l-u" type="text" placeholder="On your existing file" autocomplete="off" spellcheck="false">
<div class="fld-msg" id="l-u-m"></div>
</div>
<div class="fld">
<div class="fld-lb"><span>Password</span></div>
<div class="pw-box">
<input id="l-p" type="password" placeholder="••••••••" autocomplete="off">
<button class="pw-eye" data-t="l-p" type="button">SHOW</button>
</div>
<div class="fld-msg" id="l-p-m"></div>
</div>
<button class="btn btn-fill" id="b-log" style="width:100%" type="button">Open My File</button>
<div class="au-foot">
<span>NO FILE YET?</span>
<a class="au-sw" data-m="reg" href="#">Open one →</a>
</div>
</div>
<!-- register -->
<div class="au-pane" id="pane-reg">
<div class="fld">
<div class="fld-lb"><span>Username</span><i>320</i></div>
<input id="r-u" type="text" placeholder="Letters, numbers, underscore" autocomplete="off" spellcheck="false" maxlength="20">
<div class="fld-msg" id="r-u-m"></div>
</div>
<div class="fld">
<div class="fld-lb"><span>Password</span><i id="r-p-str"></i></div>
<div class="pw-box">
<input id="r-p" type="password" placeholder="At least 6 characters" autocomplete="off">
<button class="pw-eye" data-t="r-p" type="button">SHOW</button>
</div>
<div class="pw-str"><i id="ps0"></i><i id="ps1"></i><i id="ps2"></i><i id="ps3"></i></div>
<div class="fld-msg" id="r-p-m"></div>
</div>
<div class="fld">
<div class="fld-lb"><span>Confirm Password</span></div>
<div class="pw-box">
<input id="r-c" type="password" placeholder="Type it again" autocomplete="off">
<button class="pw-eye" data-t="r-c" type="button">SHOW</button>
</div>
<div class="fld-msg" id="r-c-m"></div>
</div>
<button class="btn btn-fill" id="b-reg" style="width:100%" type="button">File for Residency</button>
<div class="au-foot">
<span>ALREADY ON FILE?</span>
<a class="au-sw" data-m="log" href="#">Sign in →</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ══ 2. RECORDS ══ -->
<div id="sc-chars" class="screen">
<div class="spine">
<div class="spine-txt">FILE INDEX</div>
<div class="spine-mark"></div>
<div class="spine-txt">LOS SANTOS COUNTY</div>
</div>
<div style="flex:1;display:flex;flex-direction:column;min-width:0">
<div class="cs-bar">
<div class="cs-bar-l">DEPT. OF RESIDENT AFFAIRS · FILE INDEX</div>
<div class="cs-bar-l">SIGNED IN · <b id="cs-user"></b></div>
</div>
<div class="cs-body">
<div class="cs-hd">
<h2>Your Records</h2>
<p>SELECT A RECORD TO RESUME · THREE PER FILE</p>
</div>
<div class="cs-grid" id="cs-grid"></div>
</div>
</div>
</div>
<!-- ══ 3. INTAKE ══ -->
<div id="sc-create" class="screen">
<div class="cc-stage" id="cc-stage">
<div class="cc-stage-hd">
INTAKE PHOTOGRAPHY · BAY 3<br><b id="cc-recno">LS-000000</b>
</div>
<!-- SIGNATURE: builds live as the form is filled -->
<div class="cc-id">
<div class="cc-id-top"></div>
<div class="cc-id-in">
<div class="cc-id-hd">
<div class="cc-id-au">LOS SANTOS COUNTY<br><b>IDENTITY RECORD</b></div>
<div class="cc-id-no" id="id-no">LS-000000</div>
</div>
<div class="cc-id-nm pend" id="id-nm">UNNAMED</div>
<div class="cc-id-role" id="id-role">NO DECLARATION ON FILE</div>
<dl class="cc-id-tbl">
<dt>SEX</dt> <dd id="id-sex">M</dd>
<dt>BUILD</dt> <dd id="id-build">MEDIUM</dd>
<dt>COMPLEXION</dt><dd id="id-skin">III</dd>
<dt>HAIR</dt> <dd id="id-hair">01 / 01</dd>
<dt>EYES</dt> <dd id="id-eyes">01</dd>
<dt>ATTIRE</dt> <dd id="id-fit">01·01·01</dd>
</dl>
<div class="cc-id-bar" id="id-bars"></div>
</div>
<div class="cc-stampmark" id="id-stamp">Filed</div>
</div>
<div class="cc-drag"><i></i> DRAG TO ROTATE SUBJECT</div>
</div>
<div class="cc-panel">
<div class="cc-hd">
<div class="cc-hd-no">FORM LS-02 · IDENTITY RECORD</div>
<h2>New Record</h2>
<p id="cc-slotlb">SLOT 1 OF 3</p>
<div class="cc-secs">
<div class="cc-sec-t on">SEC.1 FACE</div>
<div class="cc-sec-t">SEC.2 BUILD</div>
<div class="cc-sec-t">SEC.3 ATTIRE</div>
<div class="cc-sec-t">SEC.4 NAME</div>
</div>
</div>
<div class="cc-body">
<!-- SEC 1 — face -->
<div class="cc-pane on" id="cp-0">
<div class="cc-gl">SUBJECT SEX</div>
<div class="pill-row">
<button class="pill on" data-g="0" type="button">Male</button>
<button class="pill" data-g="1" type="button">Female</button>
</div>
<div class="cc-gl">FACIAL STRUCTURE</div>
<div class="sl">
<div class="sl-lb"><span>Bone structure</span><b id="v-face_shape">50</b></div>
<input type="range" min="0" max="100" value="50" data-k="face_shape">
</div>
<div class="sl">
<div class="sl-lb"><span>Feature blend</span><b id="v-face_blend">50</b></div>
<input type="range" min="0" max="100" value="50" data-k="face_blend">
</div>
<div class="sl">
<div class="sl-lb"><span>Complexion</span><b id="v-skin_tone">50</b></div>
<input type="range" min="0" max="100" value="50" data-k="skin_tone">
</div>
<div class="cc-gl">EYE COLOUR</div>
<div class="sw-grid" id="g-eyes"></div>
</div>
<!-- SEC 2 — build -->
<div class="cc-pane" id="cp-1">
<div class="cc-gl">PHYSICAL BUILD</div>
<div class="sl">
<div class="sl-lb"><span>Body weight</span><b id="v-body_weight">50</b></div>
<input type="range" min="0" max="100" value="50" data-k="body_weight">
</div>
<div class="cc-gl">HAIR STYLE</div>
<div class="op-grid" id="g-hair"></div>
<div class="cc-gl">HAIR COLOUR</div>
<div class="sw-grid" id="g-haircol"></div>
</div>
<!-- SEC 3 — attire -->
<div class="cc-pane" id="cp-2">
<div class="cc-gl">TOP</div>
<div class="op-grid" id="g-top"></div>
<div class="cc-gl">LEGWEAR</div>
<div class="op-grid" id="g-pants"></div>
<div class="cc-gl">FOOTWEAR</div>
<div class="op-grid" id="g-shoes"></div>
</div>
<!-- SEC 4 — identity, last per brief -->
<div class="cc-pane" id="cp-3">
<div class="cc-gl">LEGAL NAME</div>
<div class="fld">
<div class="fld-lb"><span>First name</span></div>
<input id="cc-fn" type="text" placeholder="e.g. Marcus" maxlength="32" autocomplete="off">
<div class="fld-msg" id="cc-fn-m"></div>
</div>
<div class="fld">
<div class="fld-lb"><span>Last name</span></div>
<input id="cc-ln" type="text" placeholder="e.g. Rivera" maxlength="32" autocomplete="off">
<div class="fld-msg" id="cc-ln-m"></div>
</div>
<div class="cc-gl">STATEMENT OF ARRIVAL</div>
<div class="fld">
<div class="fld-lb"><span>Backstory</span><i id="cc-bs-n">0 / 600</i></div>
<textarea id="cc-bs" rows="6" maxlength="600" placeholder="Who were they before Los Santos? What did they leave behind?"></textarea>
</div>
</div>
</div>
<div class="cc-err" id="cc-err"></div>
<div class="cc-ft">
<button class="btn btn-line" id="cc-back" type="button">← Back</button>
<button class="btn btn-fill" id="cc-next" type="button">Continue</button>
</div>
</div>
</div>
<!-- ══ 4. ENTRY MAP ══ -->
<div id="sc-spawn" class="screen">
<div class="spine">
<div class="spine-txt">FORM LS-03 · POINT OF ENTRY</div>
<div class="spine-mark"></div>
<div class="spine-txt">LOS SANTOS COUNTY</div>
</div>
<div style="flex:1;display:flex;flex-direction:column;min-width:0">
<div class="sp-bar">
<div class="sp-bar-l">POINT OF ENTRY · COUNTY SURVEY GRID</div>
<div class="sp-bar-l">RECORD · <b id="sp-name"></b></div>
</div>
<div class="sp-body">
<div class="sp-mapwrap">
<div class="sp-map" id="sp-map">
<svg class="sp-coast" viewBox="0 0 100 133" preserveAspectRatio="none" aria-hidden="true">
<path d="M30 2 L52 5 L60 14 L57 26 L66 33 L78 31 L88 40 L86 55 L74 63 L70 76 L58 84
L52 96 L40 103 L34 116 L22 124 L12 120 L8 106 L14 92 L12 78 L20 66 L18 52
L10 42 L14 26 L22 12 Z"/>
</svg>
</div>
</div>
<div class="sp-side">
<div class="sp-empty" id="sp-empty">
SELECT A POINT OF ENTRY<br>
ON THE COUNTY GRID
</div>
<div class="sp-det" id="sp-det">
<div class="sp-det-no" id="sp-det-no">ENTRY 01</div>
<div class="sp-det-nm" id="sp-det-nm"></div>
<p class="sp-det-p" id="sp-det-p"></p>
<dl class="sp-det-tbl">
<dt>DISTRICT</dt> <dd id="sp-det-d"></dd>
<dt>GRID REF</dt> <dd id="sp-det-g"></dd>
<dt>DENSITY</dt> <dd id="sp-det-x"></dd>
</dl>
<div class="sp-go">
<button class="btn btn-fill" id="b-spawn" style="width:100%" type="button">Enter the County</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
'use strict';
const $ = id => document.getElementById(id);
function show (id) {
document.querySelectorAll('.screen').forEach(s => s.classList.toggle('on', s.id === id));
}
async function lua (cb, data) {
try {
const r = await fetch(`https://justrp/${cb}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data || {})
});
return await r.json();
} catch (e) { return null; }
}
function msg (el, text) {
const m = $(el);
if (!m) return;
m.textContent = text || '';
m.classList.toggle('on', !!text);
}
function busy (btn, on, label) {
btn.disabled = on;
btn.textContent = on ? label : btn.dataset.idle;
}
/* ══ NUI router — contract with client/main.lua ══ */
addEventListener('message', e => {
const { action, data } = e.data || {};
if (action === 'showAuth') show('sc-auth');
else if (action === 'authOk') onAuthOk(data);
else if (action === 'authFail') onAuthFail(data);
else if (action === 'showChars') onChars(data);
else if (action === 'charCreateFail') onCreateFail(data);
else if (action === 'showSpawn') onSpawn(data);
else if (action === 'hide') document.body.style.display = 'none';
});
/* ══════════ 1. AUTH ══════════ */
const TITLES = { log: 'Sign In', reg: 'Open a File' };
function mode (m) {
$('au-tabs').dataset.m = m;
document.querySelectorAll('.au-tab').forEach(t => t.classList.toggle('on', t.dataset.m === m));
$('pane-log').classList.toggle('on', m === 'log');
$('pane-reg').classList.toggle('on', m === 'reg');
$('au-ti').textContent = TITLES[m];
alert_('');
}
document.querySelectorAll('[data-m]').forEach(n => {
if (n.id === 'au-tabs') return;
n.addEventListener('click', ev => { ev.preventDefault(); mode(n.dataset.m); });
});
function alert_ (t) { const a = $('au-alert'); a.textContent = t || ''; a.classList.toggle('on', !!t); }
document.querySelectorAll('.pw-eye').forEach(b => {
b.addEventListener('click', () => {
const i = $(b.dataset.t);
i.type = i.type === 'password' ? 'text' : 'password';
b.textContent = i.type === 'password' ? 'SHOW' : 'HIDE';
});
});
// live validation
const U_RE = /^[a-zA-Z0-9_]+$/;
$('r-u').addEventListener('input', function () {
const v = this.value.trim();
let e = '';
if (v && v.length < 3) e = 'Too short — 3 characters minimum.';
else if (v && !U_RE.test(v)) e = 'Letters, numbers and underscore only.';
msg('r-u-m', e);
this.classList.toggle('bad', !!e);
});
const STR = ['—', 'WEAK', 'FAIR', 'STRONG', 'VERY STRONG'];
$('r-p').addEventListener('input', function () {
const v = this.value;
let s = 0;
if (v.length >= 6) s++;
if (v.length >= 10) s++;
if (/[A-Z]/.test(v) && /[a-z]/.test(v)) s++;
if (/[0-9!@#$%^&*]/.test(v)) s++;
// one accent, varied by opacity — no second hue
for (let i = 0; i < 4; i++) {
$('ps' + i).style.background = i < s
? `rgba(194,70,46,${0.4 + s * 0.15})`
: 'rgba(16,19,26,.16)';
}
$('r-p-str').textContent = STR[s];
msg('r-p-m', v && v.length < 6 ? 'At least 6 characters.' : '');
this.classList.toggle('bad', !!v && v.length < 6);
const c = $('r-c');
if (c.value) c.dispatchEvent(new Event('input'));
});
$('r-c').addEventListener('input', function () {
const bad = this.value && this.value !== $('r-p').value;
msg('r-c-m', bad ? 'Passwords do not match.' : '');
this.classList.toggle('bad', !!bad);
});
$('b-log').dataset.idle = 'Open My File';
$('b-reg').dataset.idle = 'File for Residency';
$('b-log').addEventListener('click', async () => {
const u = $('l-u').value.trim(), p = $('l-p').value;
alert_('');
if (!u || !p) return alert_('Enter your username and password.');
busy($('b-log'), true, 'Checking…');
await lua('authLogin', { username: u, password: p });
});
$('b-reg').addEventListener('click', async () => {
const u = $('r-u').value.trim(), p = $('r-p').value, c = $('r-c').value;
alert_('');
if (!u || !p || !c) return alert_('Fill in every field to file.');
if (u.length < 3 || u.length > 20) return alert_('Username must be 320 characters.');
if (!U_RE.test(u)) return alert_('Username: letters, numbers and underscore only.');
if (p.length < 6) return alert_('Password must be at least 6 characters.');
if (p !== c) return alert_('Passwords do not match.');
busy($('b-reg'), true, 'Filing…');
await lua('authRegister', { username: u, password: p });
});
[['l-u','b-log'],['l-p','b-log'],['r-u','b-reg'],['r-p','b-reg'],['r-c','b-reg']]
.forEach(([i, b]) => $(i).addEventListener('keydown', e => { if (e.key === 'Enter') $(b).click(); }));
function onAuthOk (d) {
$('cs-user').textContent = d.username;
busy($('b-log'), false); busy($('b-reg'), false);
}
function onAuthFail (d) {
alert_(d.error || 'Could not verify that file.');
busy($('b-log'), false); busy($('b-reg'), false);
}
/* ══════════ 2. RECORDS ══════════ */
function recNo (id) { return 'LS-' + String(id).padStart(6, '0'); }
function onChars (d) {
const chars = d.characters || [];
const g = $('cs-grid');
g.innerHTML = '';
for (let slot = 0; slot < 3; slot++) {
const ch = chars.find(c => c.slot === slot);
const card = document.createElement('button');
card.type = 'button';
if (ch) {
card.className = 'cs-card';
const cash = (ch.cash ?? 0) + (ch.bank ?? 0);
card.innerHTML = `
<div class="cs-card-top"></div>
<div class="cs-card-in">
<div class="cs-card-no">${recNo(ch.id)}</div>
<div class="cs-card-nm">${esc(ch.firstname)}<br>${esc(ch.lastname)}</div>
<div class="cs-card-sub">SLOT ${slot + 1} · ON FILE</div>
<dl class="cs-tbl">
<dt>SEX</dt><dd>${ch.gender === 1 ? 'F' : 'M'}</dd>
<dt>HELD</dt><dd>$${cash.toLocaleString()}</dd>
<dt>TIME</dt><dd>${Math.floor((ch.playtime || 0) / 60)}h</dd>
</dl>
</div>`;
card.addEventListener('click', () => lua('charSelect', { id: ch.id }));
} else {
card.className = 'cs-card blank';
card.innerHTML = `
<div class="cs-card-top"></div>
<div class="cs-blank-in">
<div class="cs-blank-no">SLOT ${slot + 1}</div>
<div class="cs-blank-t">No record<br>on file</div>
<div class="cs-blank-d">OPEN A NEW ONE →</div>
</div>`;
card.addEventListener('click', () => openCreate(slot));
}
g.appendChild(card);
}
show('sc-chars');
}
function esc (s) {
return String(s ?? '').replace(/[&<>"']/g, c =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
/* ══════════ 3. INTAKE ══════════ */
const D = {
slot: 0, gender: 0, firstname: '', lastname: '', backstory: '',
face_blend: .5, face_shape: .5, skin_tone: .5, eye_color: 0,
hair_style: 0, hair_color: 0, body_weight: .5,
outfit_top: 0, outfit_pants: 0, outfit_shoes: 0
};
let step = 0;
const PANES = ['cp-0', 'cp-1', 'cp-2', 'cp-3'];
const NEXT_LB = ['Continue', 'Continue', 'Continue', 'File This Record'];
const EYES = ['#4A5D78','#5B7350','#7A4A3C','#3D3A36','#6E5A38','#4E4468','#3F6B6B','#7A7040','#A8A49B','#22201E'];
const HAIRC = ['#141008','#2E1B0C','#4A2E17','#6B4626','#8C6A32','#B79A62','#D9CBA8','#9A9088','#6E6660','#3A3632','#7A2E1E','#0B0A09'];
function swatches (host, cols, key) {
const h = $(host); h.innerHTML = '';
cols.forEach((c, i) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'sw' + (i === 0 ? ' on' : '');
b.style.background = c;
b.title = `Option ${i + 1}`;
b.setAttribute('aria-label', `${key} option ${i + 1}`);
b.addEventListener('click', () => {
h.querySelectorAll('.sw').forEach(x => x.classList.remove('on'));
b.classList.add('on');
D[key] = i;
lua('applyAppearance', { [key]: i });
syncCard(key);
});
h.appendChild(b);
});
}
function options (host, n, key) {
const h = $(host); h.innerHTML = '';
for (let i = 0; i < n; i++) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'op' + (i === 0 ? ' on' : '');
b.textContent = String(i + 1).padStart(2, '0');
b.addEventListener('click', () => {
h.querySelectorAll('.op').forEach(x => x.classList.remove('on'));
b.classList.add('on');
D[key] = i;
lua('applyAppearance', { [key]: i });
syncCard(key);
});
h.appendChild(b);
}
}
swatches('g-eyes', EYES, 'eye_color');
swatches('g-haircol', HAIRC, 'hair_color');
options('g-hair', 24, 'hair_style');
options('g-top', 24, 'outfit_top');
options('g-pants', 18, 'outfit_pants');
options('g-shoes', 12, 'outfit_shoes');
// build the record-card waveform strip once
(function () {
const b = $('id-bars');
for (let i = 0; i < 46; i++) {
const s = document.createElement('i');
s.style.height = (18 + Math.sin(i * 1.7) * 12 + (i % 5) * 6) + '%';
b.appendChild(s);
}
})();
document.querySelectorAll('.pill[data-g]').forEach(p => {
p.addEventListener('click', () => {
document.querySelectorAll('.pill[data-g]').forEach(x => x.classList.remove('on'));
p.classList.add('on');
D.gender = +p.dataset.g;
lua('applyAppearance', { gender: D.gender });
syncCard('gender');
});
});
document.querySelectorAll('.cc-body input[type=range]').forEach(sl => {
const k = sl.dataset.k;
sl.addEventListener('input', () => {
D[k] = sl.value / 100;
const o = $('v-' + k); if (o) o.textContent = sl.value;
lua('applyAppearance', { [k]: D[k] });
syncCard(k);
});
});
const ROMAN = ['I','II','III','IV','V','VI'];
const BUILD = ['SLIGHT','LEAN','MEDIUM','SOLID','HEAVY'];
function syncCard (touched) {
$('id-sex').textContent = D.gender === 1 ? 'F' : 'M';
$('id-build').textContent = BUILD[Math.min(4, Math.floor(D.body_weight * 5))];
$('id-skin').textContent = ROMAN[Math.min(5, Math.floor(D.skin_tone * 6))];
$('id-hair').textContent = `${pad(D.hair_style)} / ${pad(D.hair_color)}`;
$('id-eyes').textContent = pad(D.eye_color);
$('id-fit').textContent = `${pad(D.outfit_top)}·${pad(D.outfit_pants)}·${pad(D.outfit_shoes)}`;
const map = {
gender:'id-sex', body_weight:'id-build', skin_tone:'id-skin',
hair_style:'id-hair', hair_color:'id-hair', eye_color:'id-eyes',
outfit_top:'id-fit', outfit_pants:'id-fit', outfit_shoes:'id-fit'
};
const cell = map[touched];
if (cell) {
const c = $(cell);
c.classList.remove('hit');
void c.offsetWidth; // restart the flash
c.classList.add('hit');
setTimeout(() => c.classList.remove('hit'), 620);
}
}
function pad (n) { return String((n | 0) + 1).padStart(2, '0'); }
function syncName () {
const fn = $('cc-fn').value.trim(), ln = $('cc-ln').value.trim();
const nm = $('id-nm');
if (!fn && !ln) { nm.textContent = 'UNNAMED'; nm.classList.add('pend'); }
else { nm.textContent = `${fn} ${ln}`.trim(); nm.classList.remove('pend'); }
const bs = $('cc-bs').value.trim();
$('id-role').textContent = bs
? 'STATEMENT ON FILE · ' + bs.length + ' CHR'
: 'NO DECLARATION ON FILE';
}
$('cc-fn').addEventListener('input', syncName);
$('cc-ln').addEventListener('input', syncName);
$('cc-bs').addEventListener('input', function () {
$('cc-bs-n').textContent = `${this.value.length} / 600`;
syncName();
});
function openCreate (slot) {
step = 0; D.slot = slot;
const rn = 'LS-' + String(Math.floor(100000 + slot * 137 + 421337 % 899999)).slice(0, 6);
$('id-no').textContent = rn;
$('cc-recno').textContent = rn;
$('cc-slotlb').textContent = `SLOT ${slot + 1} OF 3`;
$('id-stamp').classList.remove('hit');
syncCard(); syncName();
paint();
show('sc-create');
lua('startCharCam', {});
}
function paint () {
PANES.forEach((p, i) => $(p).classList.toggle('on', i === step));
document.querySelectorAll('.cc-sec-t').forEach((t, i) => {
t.classList.toggle('on', i === step);
t.classList.toggle('done', i < step);
});
$('cc-next').textContent = NEXT_LB[step];
$('cc-next').dataset.idle = NEXT_LB[step];
$('cc-back').textContent = step === 0 ? '← Records' : '← Back';
$('cc-err').textContent = '';
}
$('cc-next').dataset.idle = NEXT_LB[0];
$('cc-next').addEventListener('click', async () => {
$('cc-err').textContent = '';
if (step < PANES.length - 1) { step++; paint(); return; }
const fn = $('cc-fn').value.trim(), ln = $('cc-ln').value.trim();
let bad = false;
if (!fn) { msg('cc-fn-m', 'Required.'); $('cc-fn').classList.add('bad'); bad = true; }
else { msg('cc-fn-m', ''); $('cc-fn').classList.remove('bad'); }
if (!ln) { msg('cc-ln-m', 'Required.'); $('cc-ln').classList.add('bad'); bad = true; }
else { msg('cc-ln-m', ''); $('cc-ln').classList.remove('bad'); }
if (bad) { $('cc-err').textContent = 'A record needs a legal name.'; return; }
D.firstname = fn; D.lastname = ln; D.backstory = $('cc-bs').value.trim();
// the stamp lands, then the record files
$('id-stamp').classList.add('hit');
busy($('cc-next'), true, 'Filing…');
setTimeout(() => lua('charCreate', D), 480);
});
$('cc-back').addEventListener('click', () => {
if (step === 0) { lua('stopCharCam', {}); show('sc-chars'); return; }
step--; paint();
});
function onCreateFail (d) {
busy($('cc-next'), false);
$('id-stamp').classList.remove('hit');
$('cc-err').textContent = d.error || 'The county rejected that record.';
}
// drag to rotate
let drag = false, lastX = 0;
$('cc-stage').addEventListener('mousedown', e => { drag = true; lastX = e.clientX; });
addEventListener('mousemove', e => {
if (!drag) return;
lua('charCamDrag', { delta: e.clientX - lastX });
lastX = e.clientX;
});
addEventListener('mouseup', () => { drag = false; });
/* ══════════ 4. ENTRY MAP ══════════ */
// positions derived from real GTA V world coords, normalised to the grid
const ENTRIES = [
{ k:'legion', n:'Legion Square', x:40.8, y:77.3, d:'Downtown', g:'-166, -929', x2:'HIGH',
p:'The middle of everything. Traffic, noise, and more witnesses than you want.' },
{ k:'vespucci', n:'Vespucci Beach', x:13.8, y:83.2, d:'West Coast', g:'-1380, -1520', x2:'MODERATE', flip:1,
p:'Boardwalk hustle and cheap rent. Easy place to be nobody for a while.' },
{ k:'sandy', n:'Sandy Shores', x:85.4, y:31.2, d:'Blaine County', g:'1843, 3683', x2:'SPARSE', flip:1,
p:'Dust, trailers and long roads. Nothing out here happens by accident.' },
{ k:'paleto', n:'Paleto Bay', x:40.8, y:4.7, d:'North Coast', g:'-166, 6330', x2:'SPARSE',
p:'The far end of the county. Quiet, and a long way from anyone who wants you.' },
{ k:'airport', n:'LSIA Terminal', x:21.4, y:95.4, d:'South Los Santos', g:'-1037, -2738', x2:'MODERATE',
p:'Just landed, one bag, no history here yet. The cleanest start there is.' },
];
let picked = null;
(function buildMap () {
const m = $('sp-map');
ENTRIES.forEach((e, i) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'sp-pin';
b.style.left = e.x + '%';
b.style.top = e.y + '%';
if (e.flip) b.dataset.flip = '1';
b.setAttribute('aria-label', e.n);
b.innerHTML = `<span class="sp-pin-lb">${e.n.toUpperCase()}</span>`;
b.addEventListener('click', () => pick(e, i, b));
m.appendChild(b);
});
})();
function pick (e, i, node) {
document.querySelectorAll('.sp-pin').forEach(p => p.classList.remove('on'));
node.classList.add('on');
picked = e.k;
$('sp-empty').style.display = 'none';
$('sp-det').classList.add('on');
$('sp-det-no').textContent = 'ENTRY ' + String(i + 1).padStart(2, '0');
$('sp-det-nm').textContent = e.n;
$('sp-det-p').textContent = e.p;
$('sp-det-d').textContent = e.d;
$('sp-det-g').textContent = e.g;
$('sp-det-x').textContent = e.x2;
}
$('b-spawn').dataset.idle = 'Enter the County';
$('b-spawn').addEventListener('click', async () => {
if (!picked) return;
busy($('b-spawn'), true, 'Routing…');
await lua('spawnRequest', { location: picked });
});
function onSpawn (d) {
lua('stopCharCam', {});
$('sp-name').textContent = d && d.name ? d.name : '—';
show('sc-spawn');
}
</script>
</body>
</html>