massfront-playtest / src /styles /tutorial.css
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
18.7 kB
/* ============================================================================
TUTORIAL — KEEL, ship liaison intelligence
----------------------------------------------------------------------------
Visual family: same panel gradient, same border-image trick, same type
ramp as the rest of the HUD (--panelG2, --fT/--fU, the bevelled border
gradients in src/styles/ui.css's :root) — those custom properties cascade
from :root regardless of which stylesheet declared them, so reusing them
here keeps KEEL native to the HUD instead of a decal stuck on top of it.
KEEL gets its own accent (cyan, --keelB below) so its panel reads as its
own character rather than a reskinned dispatch or coach banner.
Placement: docked centre, below the goal/hero/hive cluster (ends ~105px
from the safe-area top once deployed) and below the native #toast's usual
footprint (up to ~190px for a three-line message), so KEEL's line and a
stock toast very rarely fight for the same pixels. Verified empirically
against 412×900 screenshots — see docs/TUTORIAL.md.
============================================================================ */
:root{
--keelB:linear-gradient(160deg,#bdeeff 0%,#6fd0ff 26%,#0c3a52 58%,#1a6f99 80%,#bdeeff 100%);
}
#keelWrap{
position:absolute; left:50%; top:calc(var(--sat) + 204px);
transform:translateX(-50%);
width:min(90vw,380px);
z-index:19; /* level with #coach — both are HUD-native narration, neither an overlay */
pointer-events:none;
opacity:0; visibility:hidden;
transition:opacity .28s ease;
}
#keelWrap.show{ opacity:1; visibility:visible; pointer-events:none; }
#keelWrap.show #keelBar{ pointer-events:auto; }
#keelBar{
display:flex; align-items:flex-start; gap:9px;
border-radius:12px; padding:9px 10px 9px 9px;
border:1px solid transparent;
background:var(--panelG2) padding-box, var(--keelB) border-box;
box-shadow:0 6px 20px rgba(0,0,0,.55);
transition:opacity .3s ease, transform .2s ease;
}
#keelBar.resting{ opacity:.82; transform:scale(.99); }
#keelBar.pulse{ animation:keelPulse .5s cubic-bezier(.2,.8,.3,1); }
@keyframes keelPulse{
0%{ transform:scale(.96); box-shadow:0 6px 20px rgba(0,0,0,.55), 0 0 0 0 rgba(120,220,255,.5); }
60%{ transform:scale(1.015); }
100%{ transform:scale(1); box-shadow:0 6px 20px rgba(0,0,0,.55), 0 0 0 8px rgba(120,220,255,0); }
}
/* ---------- avatar: supplied Nova commander profile under a live signal
treatment, so tutorial dialogue has a recognizable speaker ---------- */
#keelBadge{ position:relative; width:42px; height:42px; flex:0 0 42px; margin-top:1px;
border-radius:9px;background:url('../../assets/factions/nova_192.jpg') center/cover no-repeat;
border:1px solid rgba(120,220,255,.78);box-shadow:0 0 12px rgba(70,190,245,.24);overflow:hidden; }
#keelBadge .kr{
position:absolute; inset:4px; border-radius:50%;
border:1.5px solid rgba(120,220,255,.55);
}
#keelBadge .kr2{ inset:10px; border-color:rgba(120,220,255,.85); }
#keelBadge .kd{
position:absolute; left:50%; top:50%; width:5px; height:5px; margin:-2.5px 0 0 -2.5px;
border-radius:50%; background:#bdeeff; box-shadow:0 0 6px 1px rgba(120,220,255,.9);
}
#keelBar.pulse #keelBadge .kr1{ animation:keelRing 1.1s ease-out; }
#keelBar.pulse #keelBadge .kr2{ animation:keelRing 1.1s .12s ease-out; }
@keyframes keelRing{
0%{ transform:scale(.6); opacity:1; }
100%{ transform:scale(1.7); opacity:0; }
}
#keelBody{ flex:1 1 auto; min-width:0; }
#keelHd{ display:flex; align-items:baseline; gap:7px; flex-wrap:wrap; }
#keelName{
font-family:var(--fT); font-size:11.5px; font-weight:800; letter-spacing:.14em;
color:#bdeeff; text-shadow:0 0 8px rgba(65,200,255,.5);
}
#keelStepTag{
font-family:var(--fT); font-size:9px; font-weight:700; letter-spacing:.1em;
color:#7fa8c6; text-transform:uppercase;
}
#keelTxt{
margin-top:2px; font-size:12.5px; line-height:1.36; font-weight:600; color:#eaf7ff;
}
#keelProgress{
height:3px;margin-top:7px;border-radius:4px;overflow:hidden;background:rgba(4,12,20,.78);
box-shadow:inset 0 0 0 1px rgba(120,205,242,.16);
}
#keelProgress i{
display:block;width:0;height:100%;border-radius:inherit;
background:linear-gradient(90deg,#42c9ff,#ffe06b);box-shadow:0 0 7px rgba(88,205,255,.62);
transition:width .3s ease;
}
#keelNext{
display:none;align-items:center;justify-content:center;min-width:92px;min-height:44px;margin-top:8px;padding:8px 15px;
border-radius:8px;border:1px solid rgba(255,218,112,.75);color:#171104;
background:linear-gradient(180deg,#fff0a8,#dca936);box-shadow:0 0 13px rgba(255,207,75,.28);
font:900 9px/1 var(--fT);letter-spacing:.13em;
}
#keelSkip{
flex:0 0 auto; align-self:flex-start;
/* Visual glyph is small; the tap target is not. 44px is the WCAG 2.5.5
floor — verified against the rendered box, not just the source, since
box-sizing elsewhere in this codebase can make padding eat into a
min-width instead of adding to it. */
min-width:52px; min-height:44px; margin:-9px -9px 0 0;
display:flex; align-items:center; justify-content:center;
border-radius:9px; border:none; background:transparent; color:#7fa8c6;
font:800 9px/1 var(--fT);letter-spacing:.08em;
box-sizing:border-box;
}
#keelSkip:active{ background:rgba(255,255,255,.08); color:#eaf7ff; }
/* Reactive dialogue stays available during battle, but yields most of its
height to the battlefield. The full line remains in the event feed and the
card expands again as soon as combat heat falls. */
body.mfCombatHot #keelBar{align-items:center;gap:6px;padding:5px 7px;border-radius:9px}
body.mfCombatHot #keelBadge{width:30px;height:30px;flex-basis:30px;margin:0}
body.mfCombatHot #keelHd{flex-wrap:nowrap}
body.mfCombatHot #keelName{font-size:9px}
body.mfCombatHot #keelStepTag{display:none}
body.mfCombatHot #keelTxt{margin:0;font-size:10px;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
body.mfCombatHot #keelProgress,body.mfCombatHot #keelNext{display:none!important}
body.mfCombatHot #keelSkip{min-width:40px;min-height:36px;margin:-5px -7px -5px 0;font-size:8px}
/* The guided target never receives input. It only makes the existing hit box
visible, so the button/tab/card keeps its normal mobile interaction path. */
.keelFocus{
position:relative!important;z-index:22!important;
outline:4px solid #ffe06b!important;outline-offset:4px!important;
box-shadow:0 0 0 4px rgba(255,214,70,.30),0 0 30px 9px rgba(255,202,50,.82)!important;
animation:keelTarget 1.05s ease-in-out infinite!important;
}
@keyframes keelTarget{
0%,100%{filter:brightness(1);}
50%{filter:brightness(1.38);}
}
#keelMapCue{
position:fixed;left:50%;top:50%;width:82px;height:82px;transform:translate(-50%,-50%);z-index:21;
display:none;border:3px solid #ffe06b;border-radius:50%;pointer-events:none;box-sizing:border-box;
box-shadow:0 0 0 7px rgba(255,215,72,.15),0 0 26px rgba(255,204,55,.72);
animation:keelMapPulse 1.05s ease-in-out infinite;
}
#keelMapCue.show{display:block}
#keelMapCue::before,#keelMapCue::after{content:'';position:absolute;background:#ffe06b;opacity:.9}
#keelMapCue::before{width:2px;height:112px;left:37px;top:-18px}
#keelMapCue::after{height:2px;width:112px;left:-18px;top:37px}
#keelMapCue span{
position:absolute;left:50%;top:calc(100% + 10px);transform:translateX(-50%);min-width:108px;padding:5px 7px;
border-radius:7px;background:rgba(10,15,22,.94);border:1px solid rgba(255,224,107,.75);color:#ffe891;
text-align:center;white-space:nowrap;font:900 9px/1.2 var(--fT);letter-spacing:.08em;
}
#keelModeBadge{position:fixed;left:50%;top:calc(var(--sat) + 72px);transform:translateX(-50%);z-index:20;display:none;
min-width:188px;padding:7px 10px;border:1px solid rgba(255,224,107,.72);border-radius:9px;text-align:center;
background:linear-gradient(135deg,rgba(51,37,9,.96),rgba(8,21,31,.95));box-shadow:0 5px 18px rgba(0,0,0,.42),0 0 16px rgba(255,208,67,.16);pointer-events:none}
#keelModeBadge.show{display:block}#keelModeBadge b,#keelModeBadge span{display:block;font-family:var(--fT);letter-spacing:.1em}#keelModeBadge b{color:#ffe792;font-size:9px}#keelModeBadge span{margin-top:3px;color:#a9d7ef;font-size:9px;font-weight:800}
/* One coaching surface on a phone. STEP/TOTAL already lives in #keelBar, and
ordinary command notices remain recorded in the notification log. Showing
both duplicates above/below KEEL produced three stacked panels before the
player could even see the unit being discussed. Emergency attack/wave rails
are separate and remain available. */
body.trainingOperation #keelModeBadge,body.trainingOperation #toast{display:none!important}
@keyframes keelMapPulse{0%,100%{transform:translate(-50%,-50%) scale(.92)}50%{transform:translate(-50%,-50%) scale(1.06)}}
/* First-time entry point. PLAY remains a normal skirmish; this explicit card
launches the fixed, protected training ruleset. */
#keelTrainingCallout{
width:min(88vw,360px);box-sizing:border-box;margin:15px auto 0;padding:9px;border-radius:12px;text-align:left;
display:flex;align-items:center;gap:9px;
border:1px solid rgba(255,216,86,.52);background:linear-gradient(180deg,rgba(43,35,15,.94),rgba(13,20,29,.97));
box-shadow:0 5px 18px rgba(0,0,0,.42),0 0 18px rgba(255,202,55,.1);
}
.ktMiniIdentity{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.ktMiniIdentity>img{width:38px;height:38px;object-fit:contain;flex:0 0 38px}
.ktMiniIdentity>div{min-width:0}.ktMiniIdentity span,.ktMiniIdentity small{display:block;color:#ffe079;font:900 9px/1.1 var(--fT);letter-spacing:.12em}
.ktMiniIdentity b{display:block;margin:3px 0;color:#fff;font:900 10px/1.1 var(--fT);letter-spacing:.06em}.ktMiniIdentity small{color:#83a5bd;letter-spacing:.08em}
#keelTrainingCallout>button{
flex:0 0 98px;min-width:98px;min-height:48px;padding:8px;border-radius:8px;border:1px solid #ffe79a;color:#181203;
background:linear-gradient(180deg,#fff1a8,#dda837);font:900 9px/1 var(--fT);letter-spacing:.07em;
}
#keelTrainingCallout+#startBtn{margin-top:10px!important}
.keelTrainingOp{
--ktoAccent:#ffe079;position:relative;margin:0 0 13px;padding:0 12px 12px;border-radius:14px;text-align:left;overflow:hidden;
border:1px solid rgba(255,215,80,.56);background:linear-gradient(155deg,rgba(53,39,11,.92),rgba(13,21,31,.98) 48%,rgba(8,27,40,.98));
box-shadow:0 8px 22px rgba(0,0,0,.35),inset 0 1px rgba(255,255,255,.04);
}
.keelTrainingOp::after{content:'';position:absolute;right:-65px;top:42px;width:210px;height:210px;border-radius:50%;pointer-events:none;background:radial-gradient(circle,rgba(75,194,255,.12),transparent 67%)}
.keelTrainingOp.done{--ktoAccent:#8bf0b7;border-color:rgba(104,237,166,.48);background:linear-gradient(155deg,rgba(18,54,40,.82),rgba(10,23,27,.98) 52%,rgba(8,28,39,.98))}
.keelTrainingOp.active{--ktoAccent:#76ddff;border-color:rgba(89,211,255,.68)}
.ktoHead{height:38px;margin:0 -12px 10px;padding:0 12px;display:flex;align-items:center;justify-content:space-between;gap:8px;border-bottom:1px solid rgba(255,224,121,.21);background:rgba(4,12,19,.35)}
.ktoHead span{color:var(--ktoAccent);font:900 9px/1 var(--fT);letter-spacing:.13em}.ktoHead b{padding:5px 7px;border:1px solid rgba(255,224,120,.48);border-radius:7px;color:var(--ktoAccent);font:900 9px/1 var(--fT);letter-spacing:.07em;background:rgba(0,0,0,.25)}
.ktoHero{position:relative;z-index:1;display:grid;grid-template-columns:82px 1fr;gap:10px;align-items:center}.ktoPortrait{position:relative;width:82px;height:82px;border-radius:11px;overflow:hidden;border:1px solid rgba(117,214,255,.62);background:#07131e;box-shadow:0 0 15px rgba(66,190,247,.16)}
.ktoPortrait>img{width:100%;height:100%;display:block;object-fit:cover}.ktoPortrait>span{position:absolute;right:4px;bottom:4px;width:34px;height:34px;display:flex;align-items:center;justify-content:center;border-radius:8px;background:rgba(3,12,20,.88);border:1px solid rgba(116,214,255,.48)}
.ktoPortrait .facIc{width:28px!important;height:28px!important;object-fit:contain}.ktoIdentity{min-width:0}.ktoIdentity small{display:block;color:#77d8ff;font:900 9px/1 var(--fT);letter-spacing:.14em}.ktoIdentity>b{display:block;margin:4px 0;color:#fff;font:900 14px/1.05 var(--fT);letter-spacing:.07em}.ktoIdentity i{display:block;color:#d8ecf8;font:800 9px/1.2 var(--fT);font-style:normal}.ktoIdentity p{margin:5px 0 0;color:#91adbf;font-size:9px;line-height:1.35;font-weight:650}
.ktoTeach{position:relative;z-index:1;display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}.ktoTeach span{flex:1 1 29%;min-width:82px;padding:6px 5px;border-radius:7px;text-align:center;color:#a9c8da;background:rgba(3,12,20,.55);border:1px solid rgba(116,167,200,.2);font:850 9px/1 var(--fT);letter-spacing:.06em}
.ktoProgress{position:relative;z-index:1;margin-top:9px;padding:8px 9px;border-radius:9px;background:rgba(3,11,18,.58);border:1px solid rgba(119,177,212,.22)}.ktoProgress>div:first-child{display:flex;align-items:center;justify-content:space-between}.ktoProgress span{color:#8aa9bf;font:900 9px/1 var(--fT);letter-spacing:.11em}.ktoProgress b{color:var(--ktoAccent);font:900 10px/1 var(--fT)}
.ktoProgressTrack{height:5px;margin-top:6px;border-radius:5px;overflow:hidden;background:#07121d!important}.ktoProgressTrack i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#2bb9ef,var(--ktoAccent));box-shadow:0 0 8px var(--ktoAccent)}.ktoProgress small{display:block;margin-top:5px;color:#809aad;font-size:9px;line-height:1.25;font-weight:700}
.ktoFacts{position:relative;z-index:1;display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:8px}.ktoFacts div{padding:6px 3px;border-radius:8px;text-align:center;background:rgba(5,15,23,.62);border:1px solid rgba(110,167,200,.2)}.ktoFacts b,.ktoFacts span{display:block}.ktoFacts b{color:#eaf8ff;font:900 9px/1 var(--fT)}.ktoFacts span{margin-top:3px;color:#7998ad;font:800 9px/1 var(--fT);letter-spacing:.06em}
.ktoAction{position:relative;z-index:1;width:100%;min-height:50px;margin-top:9px;padding:9px;border-radius:9px;border:1px solid rgba(255,232,144,.82);color:#171204;background:linear-gradient(180deg,#fff0a1,#d5a42e);box-shadow:0 0 14px rgba(255,207,69,.16);font:900 9px/1 var(--fT);letter-spacing:.09em}.keelTrainingOp.done .ktoAction{border-color:rgba(139,240,183,.7);background:linear-gradient(180deg,#b8f5d1,#58bf88)}
@media (max-width:380px){
#keelWrap{ top:calc(var(--sat) + 196px); width:92vw; }
#keelTxt{ font-size:12px; }
#keelTrainingCallout{width:94vw}.ktMiniIdentity>img{display:none}#keelTrainingCallout>button{flex-basis:92px;min-width:92px}
.keelTrainingOp{padding-left:10px;padding-right:10px}.ktoHead{margin-left:-10px;margin-right:-10px;padding-left:10px;padding-right:10px}.ktoHero{grid-template-columns:72px 1fr}.ktoPortrait{width:72px;height:76px}.ktoTeach span{min-width:76px}
}
@media (prefers-reduced-motion:reduce){
#keelWrap{ transition:none; }
#keelBar{ transition:none; }
#keelBar.pulse{ animation:none; }
#keelBar.pulse #keelBadge .kr1,#keelBar.pulse #keelBadge .kr2{ animation:none; }
#keelProgress i{transition:none}
.keelFocus,#keelMapCue{animation:none!important}
}
/* SKIP vs END. The same button leaves guidance (harmless) or abandons a
training match (destructive). Only the destructive state is dressed as
destructive — a bordered amber chip rather than a transparent 9px hint. */
#keelSkip.keelSkipEnd{
background:rgba(255,150,60,.14); border:1px solid rgba(255,170,80,.55);
color:#ffcf9a; letter-spacing:.10em;
}
#keelSkip.keelSkipEnd:active{ background:rgba(255,150,60,.30); color:#fff; }
/* ============================================================================
WAR TABLE PRIMER — src/warprimer.js
----------------------------------------------------------------------------
The card lives INSIDE the active .mfStagePanel rather than floating over it,
so there is deliberately no position/z-index/pointer-events here: it is a
block in the stage's own flow and inherits that panel's scrolling. It reuses
--panelG2 and the HUD type ramp for the same reason KEEL does, but takes the
war table's own amber-gold accent instead of KEEL's cyan — this is table
chrome, not the ship talking.
============================================================================ */
.wtpCard{
display:block; margin:0 0 12px; padding:11px 13px 10px;
background:var(--panelG2,linear-gradient(160deg,rgba(14,22,30,.94),rgba(8,13,19,.96)));
border:1px solid rgba(255,196,110,.34); border-radius:10px;
box-shadow:inset 0 1px 0 rgba(255,220,160,.14),0 6px 18px rgba(0,0,0,.42);
}
.wtpCard>header{
display:flex; align-items:baseline; justify-content:space-between; gap:10px;
margin-bottom:6px;
}
.wtpCard>header>span{
font:600 10px/1 var(--fU,system-ui); letter-spacing:.14em; color:#ffc86e;
}
.wtpCard>header>b{
font:700 10px/1 var(--fU,system-ui); letter-spacing:.1em; color:rgba(232,240,248,.78);
white-space:nowrap;
}
.wtpCard>p{
margin:0 0 9px; font:400 12px/1.48 var(--fT,system-ui); color:rgba(232,240,248,.90);
}
.wtpFoot{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
/* The route echoes the stage stepper at the top of the screen so the card and
the nav agree on where the player is, without the card scrolling that nav
out of view being a problem. */
.wtpFoot>i{
display:flex; align-items:center; gap:4px; flex-wrap:wrap; font-style:normal;
font:600 9.5px/1.2 var(--fU,system-ui); letter-spacing:.08em; color:rgba(232,240,248,.62);
}
.wtpFoot>i>u{ text-decoration:none; color:rgba(232,240,248,.42); }
.wtpFoot>i>em{ font-style:normal; }
.wtpFoot>i>em.past{ color:rgba(255,200,110,.78); }
.wtpFoot>i>em.on{ color:#ffc86e; }
.wtpDone{
flex:0 0 auto; min-height:44px; min-width:44px; padding:0 14px;
background:rgba(255,196,110,.13); border:1px solid rgba(255,196,110,.44);
border-radius:7px; color:#ffd79a;
font:700 11px/1 var(--fU,system-ui); letter-spacing:.12em;
}
.wtpDone:active{ background:rgba(255,196,110,.30); color:#fff; }
.wtpSkip{
flex:0 0 auto; min-height:44px; padding:0 10px;
background:transparent; border:0; color:rgba(232,240,248,.78);
font:700 10px/1 var(--fU,system-ui); letter-spacing:.12em;
}
.wtpSkip:active{ color:#fff; }
.wtpCard.wtpLast{ border-color:rgba(150,255,190,.36); }
.wtpCard.wtpLast>header>span,.wtpCard.wtpLast .wtpFoot>i>em.on{ color:#9fffc4; }
.wtpCard.wtpLast .wtpDone{
background:rgba(120,255,180,.13); border-color:rgba(150,255,190,.46); color:#bfffd9;
}
@media (max-width:430px){
.wtpCard{ padding:9px 10px 9px; }
.wtpCard>p{ font-size:11.5px; }
.wtpFoot{ flex-wrap:wrap; }
.wtpFoot>i{ order:2; flex:1 1 100%; }
.wtpDone,.wtpSkip{ order:1; width:100%; }
}