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>
1204 lines
51 KiB
HTML
1204 lines
51 KiB
HTML
<!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>3–20</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 3–20 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 =>
|
||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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>
|