# Project export: AI Posture Guardian

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: AI Posture Guardian: A camera-powered desktop coach that blocks distractions until you sit up straight.
- Devpost: https://devpost.com/software/ai-posture-guardian
- GitHub: https://github.com/Sofiia-Bilyk/AI-Posture-Guardian
- Team: 1 GitHub contributor(s) — Sofiia-Bilyk (1 commits)

## Devpost submission (written by the team)

### Inspiration

AI Posture Guardian was inspired by the way laptop work slowly pulls people into bad posture without them noticing. We wanted to build a productivity tool that does more than remind you politely in the background. It actively protects your focus and your body by stepping in when your posture slips. What It Does AI Posture Guardian uses the laptop camera to detect posture in real time. When it sees sustained slouching or uneven shoulders, it shows a fullscreen reminder that blocks the screen until the user sits up straight again. The blocker can also be dismissed by pressing the spacebar twice as a manual override. How We Built It We built the project as an Electron desktop app with a React frontend. MediaPipe Pose tracks body landmarks from the laptop camera, including shoulders and ears. A posture engine evaluates head position, shoulder alignment, and shoulder tilt to decide whether the user is sitting upright. Electron powers the fullscreen always-on-top overlay, while React handles the live dashboard, sensitivity controls, warning timer, and camera preview. We also disabled Electron background throttling so posture detection keeps running even when the user is working in another app. Challenges We Faced The hardest part was tuning the posture detection so it felt helpful instead of annoying. If the thresholds were too strict, the app blocked the screen constantly. If they were too loose, it missed real slouching. We iterated on shoulder-angle detection, recovery timing, and manual unblock behavior to make the experience feel more natural. Another challenge was making the blocker work while the app was in the background. The app needed to keep detecting posture even when the user was focused on another browser or tool, so we had to adjust Electron behavior to keep the camera loop active. What We Learned We learned how to combine real-time computer vision with desktop app behavior in a way that feels immediate and practical. We also learned that UX matters a lot for wellness tools: a posture app should be firm enough to change behavior, but forgiving enough that people actually want to keep using it. What's Next Next, we would add daily posture analytics, calibration for different body types and camera angles, voice coaching, and privacy-first local session history. We could also add integrations for productivity workflows, like pausing distracting sites when posture drops or generating weekly posture improvement summaries.

## README (from the GitHub repository)

# AI Posture Guardian

AI Posture Guardian is an Electron desktop app that uses the laptop camera to detect posture in real time. If the user slouches for too long, it shows a fullscreen reminder and blocks the screen until posture improves.

## Features

- Real-time posture detection with MediaPipe Pose
- Electron fullscreen always-on-top reminder overlay
- React dashboard with camera preview and posture status
- Shoulder tilt and forward-head posture checks
- Adjustable sensitivity and warning delay
- Double-space manual unblock shortcut
- Background monitoring while working in other apps

## Tech Stack

- JavaScript
- React
- Electron
- Vite
- MediaPipe Pose
- MediaPipe Camera Utils
- MediaPipe Drawing Utils
- Web Speech API

## Run Locally

Install dependencies:

```bash
npm install
```

Start the desktop app:

```bash
npm run dev
```

When prompted, allow camera access.

## Build

```bash
npm run build
```

## Notes

Camera frames are processed locally in the app. The current MVP loads MediaPipe model assets from jsDelivr at runtime, so an internet connection is recommended for demos.


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 19 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (12 of 12)

```
.gitignore
electron/main.cjs
electron/overlay.html
electron/preload.cjs
index.html
package.json
README.md
src/App.jsx
src/main.jsx
src/PoseDetector.jsx
src/PostureEngine.js
src/styles.css
```

### Dependencies

- package.json: @mediapipe/camera_utils@^0.3.1675466862, @mediapipe/drawing_utils@^0.3.1675466124, @mediapipe/pose@^0.5.1675469404, @vitejs/plugin-react@^5.0.0, concurrently@^9.0.0, electron@^37.0.0, lucide-react@^0.468.0, react@^19.0.0, react-dom@^19.0.0, vite@^7.0.0, wait-on@^8.0.0

### Recent commits (newest first)

- Add AI Posture Guardian app

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "ai-posture-guardian",
  "version": "0.1.0",
  "private": true,
  "description": "Electron + React posture coach that uses the laptop camera to block the screen after sustained slouching.",
  "main": "electron/main.cjs",
  "scripts": {
    "dev": "concurrently -k \"vite --host 127.0.0.1\" \"wait-on http://127.0.0.1:5173 && electron .\"",
    "dev:web": "vite --host 127.0.0.1",
    "electron": "electron .",
    "build": "vite build",
    "preview": "vite preview --host 127.0.0.1"
  },
  "dependencies": {
    "@mediapipe/camera_utils": "^0.3.1675466862",
    "@mediapipe/drawing_utils": "^0.3.1675466124",
    "@mediapipe/pose": "^0.5.1675469404",
    "@vitejs/plugin-react": "^5.0.0",
    "vite": "^7.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "electron": "^37.0.0",
    "concurrently": "^9.0.0",
    "wait-on": "^8.0.0",
    "lucide-react": "^0.468.0"
  },
  "devDependencies": {}
}

```

### src/main.jsx

```javascript
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### src/App.jsx

```javascript
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Activity, Camera, Clock3, Eye, ShieldAlert, SlidersHorizontal } from "lucide-react";
import PoseDetector from "./PoseDetector.jsx";
import { evaluatePosture, getPostureMessage } from "./PostureEngine.js";

const DEFAULT_SETTINGS = {
  warningDelayMs: 7000,
  goodHoldMs: 250,
  sensitivity: 0.1,
  voiceEnabled: false
};

function formatPercent(value) {
  return `${Math.round(value * 100)}%`;
}

export default function App() {
  const [settings, setSettings] = useState(DEFAULT_SETTINGS);
  const [status, setStatus] = useState({
    quality: "waiting",
    message: "Waiting for camera permission...",
    score: 0,
    slouchMs: 0,
    goodMs: 0,
    overlayActive: false
  });
  const [stats, setStats] = useState({
    goodFrames: 0,
    badFrames: 0,
    startedAt: Date.now()
  });

  const overlayMessage = useMemo(() => getPostureMessage(status.quality), [status.quality]);

  useEffect(() => {
    window.postureGuardian?.setOverlayVisible(status.overlayActive);
    window.postureGuardian?.setOverlayMessage(overlayMessage);
  }, [overlayMessage, status.overlayActive]);

  useEffect(() => {
    return window.postureGuardian?.onManualUnblock?.(() => {
      setStatus((current) => ({
        ...current,
        slouchMs: 0,
        goodMs: 0,
        overlayActive: false
      }));
    });
  }, []);

  useEffect(() => {
    if (!settings.voiceEnabled || !status.overlayActive || !("speechSynthesis" in window)) return;

    const utterance = new SpeechSynthesisUtterance(overlayMessage);
    utterance.rate = 0.92;
    utterance.pitch = 0.95;
    window.speechSynthesis.cancel();
    window.speechSynthesis.speak(utterance);

    return () => window.speechSynthesis.cancel();
  }, [overlayMessage, settings.voiceEnabled, status.overlayActive]);

  const handlePose = useCallback(
    (landmarks, deltaMs) => {
      const result = evaluatePosture(landmarks, settings.sensitivity);

      setStats((current) => ({
        ...current,
        goodFrames: current.goodFrames + (result.good ? 1 : 0),
        badFrames: current.badFrames + (result.good ? 0 : 1)
      }));

      setStatus((current) => {
        const slouchMs = result.good ? 0 : current.slouchMs + deltaMs;
        const goodMs = result.good ? current.goodMs + deltaMs : 0;
        const shouldBlock = result.good
          ? current.overlayActive && goodMs < settings.goodHoldMs
          : slouchMs >= settings.warningDelayMs;

        return {
          quality: result.quality,
          message: result.message,
          score: result.score,
          slouchMs,
          goodMs,
          overlayActive: shouldBlock
        };
      });
    },
    [settings.goodHoldMs, settings.sensitivity, settings.warningDelayMs]
  );

  const totalFrames = stats.goodFrames + stats.badFrames;
  const goodRatio = totalFrames ? stats.goodFrames / totalFrames : 0;
  const secondsToBlock = Math.max(0, Math.ceil((settings.warningDelayMs - status.slouchMs) / 1000));

  return (
    <main className="app-shell">
      <section className="monitor">
        <div className="monitor-copy">
          <div className="eyebrow">
            <ShieldAlert size={18} />
            AI Posture Guardian
          </div>
          <h1>Camera-based posture coaching for focused laptop work.</h1>
          <p>
            The app watches shoulder and head alignment locally, warns after sustained slouching,
            then blocks the screen until your posture recovers.
          </p>
        </div>

        <PoseDetector onPose={handlePose} />
      </section>

      <section className="status-grid" aria-label="Posture status">
        <article className={`status-card status-card-${status.quality}`}>
          <div className="card-label">
            <Activity size={18} />
            Live posture
          </div>
          <strong>{status.message}</strong>
          <span>Alignment score: {formatPercent(status.score)}</span>
        </article>

        <article className="status-card">
          <div className="card-label">
            <Clock3 size={18} />
            Grace timer
          </div>
          <strong>{status.overlayActive ? "Screen blocked" : `${secondsToBlock}s`}</strong>
          <span>Bad posture must persist before the blocker appears.</span>
        </article>

        <article className="status-card">
          <div className="card-label">
            <Eye size={18} />
            Session score
          </div>
          <strong>{formatPercent(goodRatio)}</strong>
          <span>Good-posture frames since launch.</span>
        </article>
      </section>

      <section className="controls" aria-label="Posture controls">
        <div className="controls-heading">
          <SlidersHorizontal size={20} />
          <h2>Controls</h2>
        </div>

        <label>
          <span>Sensitivity</span>
          <input
            type="range"
            min="0.06"
            max="0.16"
            step="0.01"
            value={settings.sensitivity}
            onChange={(event) =>
              setSettings((current) => ({ ...current, sensitivity: Number(event.target.value) }))
            }
          />
        </label>

        <label>
          <span>Warning delay</span>
          <input
            type="range"
            min="3000"
            max="12000"
            step="1000"
            value={settings.warningDelayMs}
            onChange={(event) =>
              setSettings((current) => ({ ...current, warningDelayMs: Number(event.target.value) }))
            }
          />
        </label>

        <label className="toggle">
          <input
            type="checkbox"
            checked={settings.voiceEnabled}
            onChange={(event) =>
              setSettings((current) => ({ ...current, voiceEnabled: event.target.checked }))
            }
          />
          <span>Voice coaching</span>
        </label>
      </section>

      <footer>
        <Camera size={1
[truncated — 141 more characters]
```

### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AI Posture Guardian</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### electron/overlay.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Posture Reminder</title>
    <style>
      :root {
        color-scheme: dark;
        font-family:
          Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
          sans-serif;
      }

      body {
        margin: 0;
        min-height: 100vh;
        overflow: hidden;
        background:
          radial-gradient(circle at 50% 35%, rgba(87, 129, 92, 0.34), transparent 28rem),
          #040504;
        color: #f5f7ef;
      }

      main {
        box-sizing: border-box;
        min-height: 100vh;
        display: grid;
        place-items: center;
        padding: 8vw;
        text-align: center;
      }

      .panel {
        max-width: 980px;
      }

      .kicker {
        color: #b7d7b9;
        font-size: clamp(1rem, 1.8vw, 1.35rem);
        font-weight: 700;
        letter-spacing: 0;
        margin-bottom: 1rem;
        text-transform: uppercase;
      }

      h1 {
        margin: 0;
        font-size: clamp(3rem, 8vw, 7.5rem);
        line-height: 0.95;
        letter-spacing: 0;
      }

      p {
        margin: 2rem auto 0;
        max-width: 760px;
        color: #dce6d8;
        font-size: clamp(1.2rem, 2.5vw, 2rem);
        line-height: 1.35;
      }
    </style>
  </head>
  <body>
    <main>
      <section class="panel" aria-live="polite">
        <div class="kicker">AI Posture Guardian</div>
        <h1>Sit Up Straight</h1>
        <p id="message">Bring your shoulders back and align your ears over your shoulders to continue.</p>
      </section>
    </main>
    <script>
      let lastSpaceAt = 0;

      window.postureGuardian?.onOverlayMessage((message) => {
        document.getElementById("message").textContent = message;
      });

      window.addEventListener("keydown", (event) => {
        if (event.code !== "Space") return;

        const now = Date.now();
        if (now - lastSpaceAt <= 900) {
          window.postureGuardian?.requestManualUnblock();
          lastSpaceAt = 0;
          return;
        }

        lastSpaceAt = now;
      });
    </script>
  </body>
</html>

```

### src/PostureEngine.js

```javascript
const LEFT_EAR = 7;
const RIGHT_EAR = 8;
const LEFT_SHOULDER = 11;
const RIGHT_SHOULDER = 12;
const SHOULDER_TILT_LIMIT = 0.065;

function midpoint(a, b) {
  return {
    x: (a.x + b.x) / 2,
    y: (a.y + b.y) / 2,
    visibility: ((a.visibility ?? 1) + (b.visibility ?? 1)) / 2
  };
}

function distance(a, b) {
  return Math.hypot(a.x - b.x, a.y - b.y);
}

export function evaluatePosture(landmarks, sensitivity = 0.1) {
  if (!landmarks?.[LEFT_SHOULDER] || !landmarks?.[RIGHT_SHOULDER]) {
    return {
      good: true,
      quality: "waiting",
      score: 0,
      message: "Step into frame"
    };
  }

  const leftShoulder = landmarks[LEFT_SHOULDER];
  const rightShoulder = landmarks[RIGHT_SHOULDER];
  const leftEar = landmarks[LEFT_EAR];
  const rightEar = landmarks[RIGHT_EAR];
  const shoulders = midpoint(leftShoulder, rightShoulder);
  const ears = leftEar && rightEar ? midpoint(leftEar, rightEar) : null;
  const shoulderWidth = Math.max(0.05, distance(leftShoulder, rightShoulder));
  const shoulderTilt = Math.abs(leftShoulder.y - rightShoulder.y);

  if (!ears || shoulders.visibility < 0.55 || ears.visibility < 0.45) {
    return {
      good: true,
      quality: "waiting",
      score: 0.2,
      message: "Face the camera"
    };
  }

  const headForward = Math.abs(ears.x - shoulders.x) / shoulderWidth;
  const headDrop = Math.max(0, ears.y - shoulders.y + 0.16);
  const tiltPenalty = shoulderTilt / shoulderWidth;
  const postureLoad = headForward * 0.72 + headDrop * 0.55 + tiltPenalty * 0.25;
  const score = Math.max(0, Math.min(1, 1 - postureLoad / (sensitivity * 2.4)));
  const shouldersLevel = tiltPenalty <= SHOULDER_TILT_LIMIT;
  const good = postureLoad <= sensitivity && shouldersLevel;

  if (!good && headForward > sensitivity * 0.85) {
    return {
      good,
      quality: "forward",
      score,
      message: "Bring your head back over your shoulders"
    };
  }

  if (!shouldersLevel) {
    return {
      good,
      quality: "tilted",
      score,
      message: "Level your shoulders"
    };
  }

  if (!good) {
    return {
      good,
      quality: "slouching",
      score,
      message: "Sit up straight"
    };
  }

  return {
    good,
    quality: "good",
    score,
    message: "Posture looks good"
  };
}

export function getPostureMessage(quality) {
  switch (quality) {
    case "forward":
      return "Bring your head back over your shoulders, then relax your neck.";
    case "tilted":
      return "Level your shoulders and sit tall to continue.";
    case "slouching":
      return "Straighten your back and bring your shoulders gently back.";
    default:
      return "Bring your shoulders back and align your ears over your shoulders to continue.";
  }
}

```

### src/PoseDetector.jsx

```javascript
import React, { useEffect, useRef, useState } from "react";
import { Camera, CameraOff } from "lucide-react";
import { Camera as MediaPipeCamera } from "@mediapipe/camera_utils";
import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils";
import { POSE_CONNECTIONS, Pose } from "@mediapipe/pose";

export default function PoseDetector({ onPose }) {
  const videoRef = useRef(null);
  const canvasRef = useRef(null);
  const lastFrameAtRef = useRef(performance.now());
  const onPoseRef = useRef(onPose);
  const [cameraState, setCameraState] = useState("starting");

  useEffect(() => {
    onPoseRef.current = onPose;
  }, [onPose]);

  useEffect(() => {
    let camera;
    let pose;
    let cancelled = false;

    async function start() {
      const video = videoRef.current;
      const canvas = canvasRef.current;
      const context = canvas?.getContext("2d");

      if (!video || !canvas || !context) return;

      try {
        pose = new Pose({
          locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`
        });

        pose.setOptions({
          modelComplexity: 1,
          smoothLandmarks: true,
          enableSegmentation: false,
          minDetectionConfidence: 0.55,
          minTrackingConfidence: 0.55
        });

        pose.onResults((results) => {
          if (cancelled) return;

          context.save();
          context.clearRect(0, 0, canvas.width, canvas.height);
          context.drawImage(results.image, 0, 0, canvas.width, canvas.height);

          if (results.poseLandmarks) {
            drawConnectors(context, results.poseLandmarks, POSE_CONNECTIONS, {
              color: "#7fc97f",
              lineWidth: 3
            });
            drawLandmarks(context, results.poseLandmarks, {
              color: "#f6d365",
              lineWidth: 1,
              radius: 3
            });
          }

          context.restore();

          const now = performance.now();
          const deltaMs = Math.min(500, now - lastFrameAtRef.current);
          lastFrameAtRef.current = now;
          onPoseRef.current(results.poseLandmarks, deltaMs);
        });

        camera = new MediaPipeCamera(video, {
          width: 960,
          height: 540,
          onFrame: async () => {
            await pose.send({ image: video });
          }
        });

        await camera.start();
        setCameraState("running");
      } catch (error) {
        console.error(error);
        setCameraState("blocked");
      }
    }

    start();

    return () => {
      cancelled = true;
      camera?.stop();
      pose?.close();
    };
  }, []);

  return (
    <div className="camera-panel">
      <video ref={videoRef} className="camera-video" autoPlay playsInline muted />
      <canvas ref={canvasRef} className="pose-canvas" width="960" height="540" />
      <div className={`camera-pill camera-pill-${cameraState}`}>
        {cameraState === "blocked" ? <CameraOff size={16} /> : <Camera size={16} />}
        <span>{cameraState === "blocked" ? "Camera unavailable" : "Camera active"}</span>
      </div>
    </div>
  );
}

```

### src/styles.css

```css
:root {
  color: #182018;
  background: #f6f7f2;
  font-family:
    Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

button,
input {
  font: inherit;
}

.app-shell {
  width: min(1180px, calc(100vw - 40px));
  margin: 0 auto;
  padding: 32px 0 24px;
}

.monitor {
  display: grid;
  grid-template-columns: minmax(280px, 0.86fr) minmax(480px, 1.14fr);
  gap: 24px;
  align-items: stretch;
}

.monitor-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 420px;
}

.eyebrow,
.card-label,
.controls-heading,
footer,
.camera-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.eyebrow {
  width: fit-content;
  color: #285b34;
  font-weight: 800;
  margin-bottom: 18px;
}

h1,
h2,
p {
  margin: 0;
}

h1 {
  max-width: 760px;
  font-size: clamp(2.6rem, 5.6vw, 5.2rem);
  line-height: 0.98;
  letter-spacing: 0;
}

.monitor-copy p {
  max-width: 560px;
  margin-top: 20px;
  color: #526052;
  font-size: 1.1rem;
  line-height: 1.6;
}

.camera-panel {
  position: relative;
  overflow: hidden;
  min-height: 420px;
  border: 1px solid #d9dfd2;
  border-radius: 8px;
  background: #101510;
  box-shadow: 0 16px 40px rgba(29, 43, 28, 0.16);
}

.camera-video {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.pose-canvas {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 420px;
  object-fit: cover;
}

.camera-pill {
  position: absolute;
  left: 16px;
  top: 16px;
  padding: 8px 10px;
  border-radius: 8px;
  background: rgba(7, 10, 8, 0.78);
  color: #f4f8f0;
  font-size: 0.88rem;
  font-weight: 700;
}

.camera-pill-blocked {
  background: rgba(98, 23, 32, 0.9);
}

.status-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  margin-top: 18px;
}

.status-card,
.controls {
  border: 1px solid #dfe4d8;
  border-radius: 8px;
  background: #ffffff;
  box-shadow: 0 8px 20px rgba(29, 43, 28, 0.07);
}

.status-card {
  min-height: 156px;
  padding: 18px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.card-label {
  color: #5b6b5b;
  font-size: 0.9rem;
  font-weight: 750;
}

.status-card strong {
  color: #1d281d;
  font-size: clamp(1.35rem, 2.4vw, 2.2rem);
  line-height: 1.05;
}

.status-card span {
  color: #667366;
  line-height: 1.4;
}

.status-card-good {
  border-color: #b8d9bd;
}

.status-card-forward,
.status-card-slouching,
.status-card-tilted {
  border-color: #ecc2a6;
  background: #fff9f3;
}

.controls {
  margin-top: 14px;
  padding: 18px;
  display: grid;
  grid-template-columns: 160px repeat(2, minmax(180px, 1fr)) 180px;
  align-items: center;
  gap: 18px;
}

.controls-heading {
  color: #253025;
}

.controls h2 {
  font-size: 1.1rem;
}

.controls label:not(.toggle) {
  display: grid;
  gap: 8px;
  color: #526052;
  font-weight: 700;
}

input[type="range"] {
  accent-color: #427a4d;
  width: 100%;
}

.toggle {
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 0 14px;
  border: 1px solid #ced9ca;
  border-radius: 8px;
  color: #263226;
  font-weight: 750;
}

.toggle input {
  width: 18px;
  height: 18px;
  accent-color: #427a4d;
}

footer {
  width: fit-content;
  margin: 16px auto 0;
  color: #607060;
  font-size: 0.93rem;
}

@media (max-width: 900px) {
  .app-shell {
    width: min(100vw - 24px, 720px);
    padding-top: 18px;
  }

  .monitor,
  .status-grid,
  .controls {
    grid-template-columns: 1fr;
  }

  .monitor-copy {
    min-height: auto;
    padding: 10px 0;
  }

  .camera-panel,
  .pose-canvas {
    min-height: 320px;
  }
}

```