diff --git a/fiveparaminte-main/contec_parser.py b/fiveparaminte-main/contec_parser.py index 4116255..37b17e6 100644 --- a/fiveparaminte-main/contec_parser.py +++ b/fiveparaminte-main/contec_parser.py @@ -58,13 +58,14 @@ CONTEC_PARAM_MAP = { # Temperature "TEMP": "temperature", "TEMP1": "temperature", - "TEMP2": "temperature", + "TEMP2": "temperature2", "T1": "temperature", - "T2": "temperature", + "T2": "temperature2", "BODY_TEMP": "temperature", "TEMP-1": "temperature", - "TEMP-2": "temperature", + "TEMP-2": "temperature2", "BODYTEMP": "temperature", + "TD": "temperature_diff", } # Sentinel value used by Contec to mark an invalid/disconnected parameter @@ -304,7 +305,7 @@ def _parse_45_byte_packet(data: bytes, vitals_dict: dict) -> bool: def _parse_56_byte_packet(data: bytes, vitals_dict: dict) -> bool: """ - Parse 56-byte packet (Subtype 23) — contains float32 Temperature values. + Parse 56-byte packet (Subtype 23) — contains float32 Temperature values for T1 and T2. Verified layout: [8-11] Temperature 1 (float32) @@ -318,8 +319,6 @@ def _parse_56_byte_packet(data: bytes, vitals_dict: dict) -> bool: if "present_fields" not in vitals_dict: vitals_dict["present_fields"] = [] - found = False - temp1 = read_f32_le(data, 8) temp2 = read_f32_le(data, 12) @@ -327,24 +326,24 @@ def _parse_56_byte_packet(data: bytes, vitals_dict: dict) -> bool: # Disconnected sentinel is 9999.0 or 3276.7 (0x7FFF) return t is not None and is_valid_f32(t) and abs(t - 3276.7) > 1.0 and 20.0 < t < 45.0 - if is_real_temp(temp1): - vitals_dict["temperature"] = round(temp1, 1) - if "temperature" not in vitals_dict["present_fields"]: - vitals_dict["present_fields"].append("temperature") - found = True - elif is_real_temp(temp2): - vitals_dict["temperature"] = round(temp2, 1) - if "temperature" not in vitals_dict["present_fields"]: - vitals_dict["present_fields"].append("temperature") - found = True - elif temp1 is not None or temp2 is not None: - # Sensor disconnected / off - vitals_dict["temperature"] = None - if "temperature" not in vitals_dict["present_fields"]: - vitals_dict["present_fields"].append("temperature") - found = True + t1_val = round(temp1, 1) if is_real_temp(temp1) else None + t2_val = round(temp2, 1) if is_real_temp(temp2) else None - return found + vitals_dict["temperature"] = t1_val + vitals_dict["temperature2"] = t2_val + if t1_val is not None and t2_val is not None: + vitals_dict["temperature_diff"] = round(abs(t1_val - t2_val), 1) + else: + vitals_dict["temperature_diff"] = None + + if "temperature" not in vitals_dict["present_fields"]: + vitals_dict["present_fields"].append("temperature") + if "temperature2" not in vitals_dict["present_fields"]: + vitals_dict["present_fields"].append("temperature2") + if "temperature_diff" not in vitals_dict["present_fields"]: + vitals_dict["present_fields"].append("temperature_diff") + + return (t1_val is not None or t2_val is not None) def _parse_spo2_waveform_packet(data: bytes, vitals_dict: dict) -> bool: diff --git a/fiveparaminte-main/contec_server.py b/fiveparaminte-main/contec_server.py index 44372c0..54a4f44 100644 --- a/fiveparaminte-main/contec_server.py +++ b/fiveparaminte-main/contec_server.py @@ -598,6 +598,8 @@ async def _persist_and_forward_bg(vitals, should_persist): map_bp=vitals.map_bp, respiratory_rate=vitals.respiratory_rate, temperature=vitals.temperature, + temperature2=vitals.temperature2, + temperature_diff=vitals.temperature_diff, transmitted=False ) db.add(reading) diff --git a/fiveparaminte-main/models.py b/fiveparaminte-main/models.py index a4d02e2..b03be40 100644 --- a/fiveparaminte-main/models.py +++ b/fiveparaminte-main/models.py @@ -34,6 +34,8 @@ class VitalReading(Base): map_bp = Column(Float, nullable=True) respiratory_rate = Column(Float, nullable=True) temperature = Column(Float, nullable=True) + temperature2 = Column(Float, nullable=True) + temperature_diff = Column(Float, nullable=True) transmitted = Column(Boolean, default=False, index=True) class TransmissionLog(Base): diff --git a/fiveparaminte-main/routers/dashboard.py b/fiveparaminte-main/routers/dashboard.py index 3b43f32..9b41a48 100644 --- a/fiveparaminte-main/routers/dashboard.py +++ b/fiveparaminte-main/routers/dashboard.py @@ -70,7 +70,14 @@ def serve_dashboard(): dashboard_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "dashboard.html") try: with open(dashboard_path, "r", encoding="utf-8") as f: - return HTMLResponse(content=f.read()) + return HTMLResponse( + content=f.read(), + headers={ + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0" + } + ) except FileNotFoundError: return HTMLResponse(content="
static/dashboard.html is missing
", status_code=404) @@ -192,6 +199,8 @@ def list_patients(db: Session = Depends(get_db)): "map_bp": latest_reading.map_bp if latest_reading else None, "respiratory_rate": latest_reading.respiratory_rate if latest_reading else None, "temperature": latest_reading.temperature if latest_reading else None, + "temperature2": getattr(latest_reading, 'temperature2', None) if latest_reading else None, + "temperature_diff": getattr(latest_reading, 'temperature_diff', None) if latest_reading else None, "timestamp": latest_reading.timestamp.isoformat() if (latest_reading and latest_reading.timestamp) else None, } if latest_reading else None }) diff --git a/fiveparaminte-main/schemas.py b/fiveparaminte-main/schemas.py index ac2c884..0cd2556 100644 --- a/fiveparaminte-main/schemas.py +++ b/fiveparaminte-main/schemas.py @@ -14,6 +14,8 @@ class NormalizedVitals(BaseModel): map_bp: Optional[float] = None respiratory_rate: Optional[float] = None temperature: Optional[float] = None + temperature2: Optional[float] = None + temperature_diff: Optional[float] = None present_fields: Optional[list] = None # Raw waveform sample arrays (8-bit unsigned, 0-255) ecg_wave: Optional[List[int]] = None # ECG waveform samples from 989-byte packet diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html index 3ffdc4f..35e1d46 100644 --- a/fiveparaminte-main/static/dashboard.html +++ b/fiveparaminte-main/static/dashboard.html @@ -100,9 +100,11 @@ /* === Minimal / Embedded Iframe Mode === */ body.minimal-mode { - background: #090d16 !important; + background: #03060c !important; min-height: 100vh; overflow-x: hidden; + margin: 0 !important; + padding: 0 !important; } body.minimal-mode .bg-glow { @@ -110,33 +112,23 @@ } body.minimal-mode .app-container { - padding: 8px 12px !important; + padding: 8px !important; max-width: 100% !important; - min-height: auto !important; + min-height: 100vh !important; + margin: 0 !important; } body.minimal-mode .header, body.minimal-mode #setupGuide, body.minimal-mode #patientFilterToolbar, body.minimal-mode #deviceNavBar, - body.minimal-mode .info-bar { + body.minimal-mode .info-bar, + body.minimal-mode #singleTargetWaitingState, + body.minimal-mode #singlePatientBanner, + body.minimal-mode #singleVitalsGrid { display: none !important; } - body.minimal-mode .patient-banner { - margin-bottom: 12px !important; - padding: 10px 16px !important; - } - - body.minimal-mode .patient-banner button { - display: none !important; - } - - body.minimal-mode .vitals-grid { - gap: 12px !important; - margin-bottom: 8px !important; - } - /* === Header === */ .header { display: flex; @@ -1280,6 +1272,774 @@ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } + + /* === ICU Live Patient Monitor Console Styles === */ + .icu-monitor { + display: flex; + flex-direction: column; + background: #050811; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 16px; + overflow: hidden; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.1); + margin-bottom: 20px; + position: relative; + } + + body.minimal-mode .icu-monitor { + border-radius: 12px; + margin-bottom: 0; + border: 1px solid rgba(255, 255, 255, 0.15); + min-height: calc(100vh - 16px); + } + + .icu-monitor-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + background: linear-gradient(180deg, #111a2e 0%, #0c1220 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + flex-wrap: wrap; + gap: 12px; + } + + .icu-patient-info { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; + } + + .icu-patient-avatar { + width: 44px; + height: 44px; + border-radius: 10px; + background: linear-gradient(135deg, #0ea5e9, #6366f1); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + color: white; + font-weight: bold; + box-shadow: 0 0 16px rgba(14, 165, 233, 0.4); + } + + .icu-patient-name { + font-size: 19px; + font-weight: 800; + color: #ffffff; + letter-spacing: 0.3px; + display: flex; + align-items: center; + gap: 10px; + } + + .icu-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 700; + font-family: 'JetBrains Mono', monospace; + } + + .icu-tag-mrn { + background: rgba(6, 182, 212, 0.18); + color: #38bdf8; + border: 1px solid rgba(6, 182, 212, 0.35); + } + + .icu-tag-bed { + background: rgba(168, 85, 247, 0.18); + color: #c084fc; + border: 1px solid rgba(168, 85, 247, 0.35); + } + + .icu-tag-live { + background: rgba(16, 185, 129, 0.2); + color: #34d399; + border: 1px solid rgba(16, 185, 129, 0.4); + animation: liveBeacon 2s ease-in-out infinite; + } + + @keyframes liveBeacon { + 0%, 100% { box-shadow: 0 0 4px rgba(16, 185, 129, 0.3); } + 50% { box-shadow: 0 0 14px rgba(16, 185, 129, 0.8); } + } + + .icu-header-controls { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + + .heart-beat-indicator { + width: 34px; + height: 34px; + border-radius: 50%; + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + transition: transform 0.08s, box-shadow 0.08s, background 0.08s; + cursor: pointer; + } + + .heart-beat-indicator.beat { + transform: scale(1.35); + background: rgba(239, 68, 68, 0.45); + box-shadow: 0 0 18px #ef4444; + } + + .icu-btn { + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.15); + color: #e2e8f0; + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s; + outline: none; + } + + .icu-btn:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); + color: #fff; + } + + .icu-btn.active { + background: rgba(6, 182, 212, 0.25); + border-color: #06b6d4; + color: #38bdf8; + box-shadow: 0 0 12px rgba(6, 182, 212, 0.35); + } + + .icu-time-display { + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + font-weight: 700; + color: #94a3b8; + background: rgba(0, 0, 0, 0.4); + padding: 6px 10px; + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.08); + } + + /* Main Waveform Channels */ + .icu-waveform-stage { + display: flex; + flex-direction: column; + background: #02050b; + flex: 1; + } + + .icu-channel { + display: grid; + grid-template-columns: 1fr 220px; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); + background: #02050c; + min-height: 140px; + position: relative; + } + + @media (max-width: 768px) { + .icu-channel { + grid-template-columns: 1fr 140px; + min-height: 110px; + } + } + + .icu-channel-canvas-wrap { + position: relative; + width: 100%; + height: 100%; + min-height: 130px; + overflow: hidden; + background: #02050b; + } + + .icu-channel-canvas-wrap canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: block; + } + + .icu-channel-lead-info { + position: absolute; + top: 8px; + left: 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + pointer-events: none; + z-index: 2; + display: flex; + align-items: center; + gap: 8px; + } + + .lead-tag { + padding: 1px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 800; + } + + .icu-channel-param { + display: flex; + flex-direction: column; + justify-content: center; + padding: 12px 18px; + border-left: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(9, 14, 26, 0.8); + backdrop-filter: blur(8px); + position: relative; + } + + .param-title { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .param-value-wrap { + display: flex; + align-items: baseline; + gap: 6px; + margin: 4px 0; + } + + .param-value-num { + font-family: 'JetBrains Mono', monospace; + font-size: 52px; + font-weight: 800; + line-height: 1; + letter-spacing: -2px; + } + + .param-unit { + font-size: 16px; + font-weight: 600; + opacity: 0.75; + } + + .param-sub { + font-size: 11px; + font-family: 'JetBrains Mono', monospace; + color: #94a3b8; + display: flex; + justify-content: space-between; + align-items: center; + } + + /* Channel 1: ECG (Lead II) */ + .icu-channel-ecg .icu-channel-lead-info { color: #00ff75; } + .icu-channel-ecg .lead-tag { background: rgba(0, 255, 117, 0.2); color: #00ff75; } + .icu-channel-ecg .param-title { color: #00ff75; } + .icu-channel-ecg .param-value-num { color: #00ff75; text-shadow: 0 0 20px rgba(0, 255, 117, 0.5); } + + /* Channel 2: SpO2 PLETH */ + .icu-channel-spo2 .icu-channel-lead-info { color: #00e5ff; } + .icu-channel-spo2 .lead-tag { background: rgba(0, 229, 255, 0.2); color: #00e5ff; } + .icu-channel-spo2 .param-title { color: #00e5ff; } + .icu-channel-spo2 .param-value-num { color: #00e5ff; text-shadow: 0 0 20px rgba(0, 229, 255, 0.5); } + + /* Channel 3: RESP */ + .icu-channel-resp .icu-channel-lead-info { color: #ffbe0b; } + .icu-channel-resp .lead-tag { background: rgba(255, 190, 11, 0.2); color: #ffbe0b; } + .icu-channel-resp .param-title { color: #ffbe0b; } + .icu-channel-resp .param-value-num { color: #ffbe0b; text-shadow: 0 0 20px rgba(255, 190, 11, 0.5); } + + /* Perfusion index bar meter */ + .pi-meter { + display: flex; + gap: 3px; + margin-top: 4px; + } + + .pi-segment { + height: 4px; + flex: 1; + border-radius: 1px; + background: rgba(0, 229, 255, 0.15); + transition: background 0.15s; + } + + .pi-segment.active { + background: #00e5ff; + box-shadow: 0 0 6px #00e5ff; + } + + /* Bottom Parameters Row (NIBP, TEMP, MAP) */ + .icu-footer-params { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + background: #060b16; + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding: 12px 18px; + gap: 16px; + } + + @media (max-width: 768px) { + .icu-footer-params { + grid-template-columns: 1fr; + gap: 10px; + } + } + + .icu-footer-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 10px; + padding: 10px 14px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .icu-footer-card.nibp { border-left: 3px solid #c084fc; } + .icu-footer-card.temp { border-left: 3px solid #fb923c; } + .icu-footer-card.map { border-left: 3px solid #f43f5e; } + + .icu-footer-card .card-head { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.8px; + color: #94a3b8; + display: flex; + align-items: center; + justify-content: space-between; + } + + .icu-footer-card .card-val { + font-family: 'JetBrains Mono', monospace; + font-size: 32px; + font-weight: 800; + line-height: 1.1; + margin: 4px 0; + } + + .icu-footer-card.nibp .card-val { color: #c084fc; text-shadow: 0 0 15px rgba(192, 132, 252, 0.4); } + .icu-footer-card.temp .card-val { color: #fb923c; text-shadow: 0 0 15px rgba(251, 146, 60, 0.4); } + .icu-footer-card.map .card-val { color: #f43f5e; text-shadow: 0 0 15px rgba(244, 63, 94, 0.4); } + + .icu-footer-card .card-sub { + font-size: 11px; + font-family: 'JetBrains Mono', monospace; + color: #64748b; + } + + /* Trends Container */ + .icu-trends-stage { + display: none; + flex-direction: column; + gap: 16px; + padding: 20px; + background: #02050b; + flex: 1; + } + + .icu-trend-chart-wrap { + background: #070d18; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + padding: 14px; + } + + .icu-trend-title { + font-size: 13px; + font-weight: 700; + margin-bottom: 8px; + display: flex; + align-items: center; + justify-content: space-between; + } + + .icu-trend-canvas { + width: 100%; + height: 130px; + display: block; + } + + + /* ========================================================================= + CONTEC CMS7000PLUS / CMS8500 EXACT PATIENT MONITOR MACHINE STYLING + ========================================================================= */ + .contec-machine-screen { + background: #000000; + border: 2px solid #1e293b; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 0 35px rgba(0,0,0,0.95), inset 0 0 10px rgba(0,0,0,0.8); + display: flex; + flex-direction: column; + width: 100%; + min-height: 560px; + font-family: 'JetBrains Mono', -apple-system, sans-serif; + color: #ffffff; + user-select: none; + position: relative; + } + + body.minimal-mode .contec-machine-screen { + border-radius: 0; + border: none; + min-height: 100vh; + } + + /* Top Machine Bar */ + .machine-top-bar { + display: flex; + align-items: center; + justify-content: space-between; + background: #090e18; + border-bottom: 1px solid #1e293b; + padding: 8px 14px; + font-size: 12px; + font-weight: 700; + gap: 10px; + flex-wrap: wrap; + } + + .machine-brand { + color: #facc15; + font-weight: 900; + letter-spacing: 1.5px; + font-size: 13px; + display: flex; + align-items: center; + gap: 6px; + } + + .machine-pill { + background: #1e293b; + color: #38bdf8; + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + border: 1px solid #334155; + } + + .machine-alarm-normal { + background: rgba(16, 185, 129, 0.2); + color: #34d399; + border: 1px solid rgba(16, 185, 129, 0.4); + padding: 3px 8px; + border-radius: 4px; + font-size: 11px; + animation: beaconGlow 2s infinite ease-in-out; + } + + @keyframes beaconGlow { + 0%, 100% { box-shadow: 0 0 4px rgba(16, 185, 129, 0.3); } + 50% { box-shadow: 0 0 12px rgba(16, 185, 129, 0.8); } + } + + .machine-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .machine-btn { + background: #1e293b; + border: 1px solid #334155; + color: #cbd5e1; + padding: 4px 10px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + transition: all 0.15s; + } + + .machine-btn:hover { + background: #334155; + color: #fff; + } + + .machine-btn.active { + background: #0284c7; + border-color: #38bdf8; + color: #fff; + } + + /* Machine Screen Body: Left = Waveforms, Right = Parameters */ + .machine-body { + display: grid; + grid-template-columns: 1fr 270px; + background: #000000; + flex: 1; + position: relative; + } + + @media (max-width: 900px) { + .machine-body { + grid-template-columns: 1fr 210px; + } + } + + /* Left: Waveforms Column (3 stacked rows) */ + .machine-waveforms-col { + display: flex; + flex-direction: column; + border-right: 1px solid #1e293b; + background: #000000; + } + + .wave-row { + flex: 1; + min-height: 140px; + position: relative; + border-bottom: 1px solid #111827; + overflow: hidden; + background: #000000; + } + + .wave-row canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: block; + } + + .wave-header { + position: absolute; + top: 6px; + left: 10px; + font-size: 12px; + font-weight: 800; + z-index: 2; + pointer-events: none; + display: flex; + align-items: center; + gap: 8px; + text-shadow: 0 0 8px rgba(0,0,0,0.9); + } + + /* Calibration pulse bracket on left of ECG: [|--- 1mV ---|] */ + .ecg-cal-pulse { + position: absolute; + left: 6px; + top: 50%; + transform: translateY(-50%); + width: 10px; + height: 38px; + border: 1.5px solid #00ff00; + border-right: none; + z-index: 2; + pointer-events: none; + opacity: 0.85; + } + + /* Right: Parameter Blocks (Exact Contec Layout) */ + .machine-params-col { + display: flex; + flex-direction: column; + background: #000000; + } + + .param-box { + flex: 1; + border-bottom: 1px solid #1e293b; + padding: 8px 12px; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + background: #000000; + } + + .param-box-top { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.5px; + } + + .param-limits { + font-size: 11px; + color: #64748b; + font-weight: 600; + } + + .param-box-main { + display: flex; + align-items: center; + justify-content: space-between; + margin: 2px 0; + } + + .param-giant-num { + font-size: 58px; + font-weight: 900; + line-height: 1; + letter-spacing: -2px; + font-family: 'JetBrains Mono', monospace; + } + + .param-unit-label { + font-size: 13px; + font-weight: 700; + margin-left: 4px; + } + + .param-box-bottom { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + font-weight: 600; + color: #94a3b8; + } + + /* Exact Contec Colors */ + .param-hr { color: #00ff00; } + .param-hr .param-giant-num { color: #00ff00; text-shadow: 0 0 14px rgba(0,255,0,0.65); } + + .param-spo2 { color: #00ffff; } + .param-spo2 .param-giant-num { color: #00ffff; text-shadow: 0 0 14px rgba(0,255,255,0.65); } + + .param-resp { color: #ffff00; } + .param-resp .param-giant-num { color: #ffff00; text-shadow: 0 0 14px rgba(255,255,0,0.65); } + + /* Vertical SpO2 LED Pulse Bar (Exact Contec Machine) */ + .vertical-pulse-bar { + display: flex; + flex-direction: column-reverse; + gap: 2px; + width: 14px; + height: 52px; + padding: 2px; + background: #080d16; + border: 1px solid rgba(0, 255, 255, 0.3); + border-radius: 2px; + } + + .v-segment { + flex: 1; + background: rgba(0, 255, 255, 0.12); + border-radius: 1px; + transition: background 0.08s; + } + + .v-segment.lit { + background: #00ffff; + box-shadow: 0 0 6px #00ffff; + } + + /* Bottom Row: NIBP & TEMP (Exact Contec Layout) */ + .machine-bottom-row { + display: grid; + grid-template-columns: 1fr 1fr; + background: #070c18; + border-top: 1px solid #1e293b; + padding: 8px 14px; + gap: 12px; + } + + .nibp-block { + color: #ffffff; + border-right: 1px solid #1e293b; + padding-right: 12px; + } + + .nibp-title { + font-size: 11px; + font-weight: 800; + color: #d8b4fe; + display: flex; + justify-content: space-between; + } + + .nibp-values { + font-size: 34px; + font-weight: 900; + font-family: 'JetBrains Mono', monospace; + color: #ffffff; + margin: 2px 0; + display: flex; + align-items: baseline; + gap: 6px; + text-shadow: 0 0 10px rgba(255,255,255,0.4); + } + + .temp-block { + color: #ff9900; + padding-left: 6px; + } + + .temp-title { + font-size: 11px; + font-weight: 800; + color: #ff9900; + display: flex; + justify-content: space-between; + } + + .temp-values { + font-size: 34px; + font-weight: 900; + font-family: 'JetBrains Mono', monospace; + color: #ff9900; + margin: 2px 0; + display: flex; + align-items: baseline; + gap: 6px; + text-shadow: 0 0 10px rgba(255,153,0,0.4); + } + + .alarm-banner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(239, 68, 68, 0.25); + border: 1px solid #ef4444; + color: #fca5a5; + padding: 6px 14px; + border-radius: 4px; + font-weight: 800; + font-size: 13px; + letter-spacing: 1px; + display: none; + z-index: 5; + animation: alarmFlash 1s infinite alternate; + } + + @keyframes alarmFlash { + 0% { opacity: 0.6; } + 100% { opacity: 1; } + } + @@ -1424,175 +2184,190 @@ +