Spaces:
Running
Running
| /* ============================================================================ | |
| 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} | |
| 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;z-index:22; | |
| outline:4px solid #ffe06b;outline-offset:4px; | |
| box-shadow:0 0 0 4px rgba(255,214,70,.30),0 0 30px 9px rgba(255,202,50,.82); | |
| animation:keelTarget 1.05s ease-in-out infinite; | |
| } | |
| @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} | |
| @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} | |
| .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;height:28px;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}.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} | |
| } | |
| /* 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%; } | |
| } | |