PURPOSE

Draws the in-mission “SURVIVE M:SS until extraction” reminder banner above the bottom weapon-slot row. Frames the mission-timer loss condition positively as time-until-extraction rather than time-until-death. Includes entrance slide/fade/scale, mirrored exit, and an urgency pulse on the accent stripe and time number when the timer drops under the urgent threshold.

OWNS

  • Module-private banner entrance state (_introStartUiTime) tracking when the banner first became visible this run.
  • Banner geometry constants: width, height, side safety margin, weapon-row clearance math.
  • Entrance / exit animation tunings: durations, slide offsets, scale endpoints, easing functions (_easeOutCubic, _easeInCubic).
  • Urgency pulse tunings: pulse rate, amplitude for stripe and time number, urgency threshold.
  • Typography constants and the M:SS string formatter (_formatMmSs).
  • Banner Y-position calculation that anchors above the largest plausible weapon row, clearing the fire-flash ring and ready glow.

READS FROM

  • game.phase, game.missionTimer, game.missionTimerMax, game.uiTime, game.time from ../core.
  • ship.alive from ../core.
  • uiScale, W, H from ../core for layout.
  • MedicalPalette tokens (borderStrong, textInverse, accentBright, statusBad, statusWarn) and medFontBody from ./medical-canvas-palette.
  • Optional yOverride parameter for test placement.

PUSHES TO

  • The supplied CanvasRenderingContext2D — paints background pill, four-edge border, accent stripe, and three text segments (label, time, suffix).
  • Returns the banner height in screen px (0 when not drawn) to the caller.
  • Restores ctx.textBaseline, ctx.textAlign, and ctx.globalAlpha to drawHUD’s expected baseline before returning.

DOES NOT

  • Does not own or mutate game.missionTimer — purely a renderer.
  • Does not draw on the menu, level-up, or cinematic phases, when the player is dead, when the mission is untimed (missionTimerMax <= 0), or once the timer has expired (overtime path handles its own messaging).
  • Does not use gradients, shadowBlur, or clip — sticks to the 2ms HUD perf budget.
  • Does not animate during pause — the urgency pulse uses game.time, which freezes when the game pauses.
  • Does not stroke its border — uses four fillRect edges instead, since axis-aligned strokeRect would cost more path ops than one fillRect ring.

Signals

  • urgent flips true when game.missionTimer <= URGENT_THRESHOLD_SEC (30s), driving stripe colour swap to statusBad, time-number swap to statusWarn, and a sine pulse at URGENT_PULSE_RATE (8 rad/s) on both — matched to the top-right HUD timer pulse so the two views read as the same cue.
  • inOutro flips true when game.missionTimer < OUTRO_FADE_WINDOW_SEC (0.45s), starting the mirrored exit slide-down + shrink + fade.
  • Composite alpha below 0.005 short-circuits the draw entirely.
  • _introStartUiTime resets to null whenever the banner becomes ineligible, so the entrance replays on every new mission, respawn, or eligibility flip.

Entry points

  • drawMissionTimerBanner(ctx, yOverride?) — main draw call, invoked by the HUD renderer each frame. Returns the rendered banner height in screen px.
  • getMissionTimerBannerY() — exported helper that returns the resting top-Y of the banner; lets the HUD reason about banner placement without redoing the layout math.
  • resetMissionTimerBanner() — exported state-reset for the bridge to call at run start / run end (mirrors clearArtifactBanners); also useful for tests that need a clean entrance between cases.

Pattern notes

  • First frame after eligibility seeds _introStartUiTime biased ~1 frame into the past (FIRST_FRAME_BIAS = 0.016) so the entrance never paints a fully transparent frame.
  • Entrance uses _easeOutCubic (fast-then-settle); exit uses _easeInCubic (slow-then-accelerate). Asymmetric easing keeps the exit from feeling mechanical.
  • Entrance slide distance is clamped in device-px (INTRO_SLIDE_OFFSET_DEVICE_MAX = 22) so high-uiScale phones don’t experience a heavy 40+ device-px lurch.
  • Scale transforms around the banner center (translate / scale / translate) so the pop reads from the middle rather than a corner.
  • Three-segment text layout (SURVIVE / M:SS / until extraction) lets the time number carry its own larger font size, brighter colour, and independent urgency alpha pulse — visual hierarchy puts the number first at mobile glance distance.
  • Banner width clamps to max(80, W - sideSafety*2) so narrow split / playground viewports never overflow.
  • Banner Y-position is computed bottom-up from H, subtracting bottom margin, weapon-row gap, slot diameter, glow clearance, pad, and banner height — each term constant-defined and uiScale-aware so it tracks drawHUD’s weapon-row layout exactly.
  • Typography routes through medFontBody(size, FONT_WEIGHT) at weight 600 so the banner inherits the medical-UI font family/fallback stack and reads as part of the HUD type system.
  • Background stays semi-opaque dark (rgba(8, 12, 20, 0.78)) rather than the medical white surface, because the banner sits on the live game canvas and a full-white panel would over-bright the run-feel.