* { box-sizing: border-box; } html, body { margin: 0; height: 100%; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f0f12; color: #e8e8ec; user-select: none; } body { display: flex; flex-direction: column; overflow: hidden; } .top-edge-hit { display: none; } .toolbar { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: #1a1a20; border-bottom: 1px solid #2a2a32; flex-shrink: 0; overflow: hidden; max-height: 72px; opacity: 1; transition: max-height 0.22s ease, padding 0.22s ease, opacity 0.22s ease, transform 0.22s ease, border-color 0.22s ease; } .toolbar-spacer { flex: 1; } .app-version { flex-shrink: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; color: #6f6f7d; white-space: nowrap; user-select: text; } .layout-controls { display: flex; align-items: center; gap: 10px; } .layout-field { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #a0a0ae; } .layout-field input { width: 64px; padding: 7px 8px; border-radius: 8px; border: 1px solid #3a3a44; background: #25252e; color: #e8e8ec; font-size: 14px; } .layout-field input:focus { outline: none; border-color: #4a6fa5; } .btn { padding: 8px 14px; border-radius: 8px; border: 1px solid #3a3a44; background: #25252e; color: #e8e8ec; font-size: 14px; cursor: pointer; touch-action: manipulation; } .btn:hover { background: #32323c; } .btn.primary { background: #3d5a80; border-color: #4a6fa5; } .btn.primary:hover { background: #4a6fa5; } .btn.btn-small { padding: 6px 10px; font-size: 12px; } .app-main { flex: 1; min-height: 0; display: flex; align-items: stretch; } .stage { flex: 1; min-width: 0; position: relative; display: flex; align-items: stretch; justify-content: stretch; padding: 12px 14px 12px 16px; overflow: hidden; } .stage.drag-over { outline: 2px dashed #4a6fa5; outline-offset: -6px; background: #15151a; } .row { width: 100%; height: 100%; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr)); gap: 10px; } .stage-slot { min-width: 0; min-height: 0; position: relative; border-radius: 12px; overflow: hidden; background: #15151b; border: 2px solid #282832; } .stage-slot.empty-slot { border-style: dashed; border-color: #343442; background: rgba(30, 30, 38, 0.65); } .stage-slot.empty-slot::before { content: attr(data-slot-label); position: absolute; top: 8px; left: 10px; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #646474; } .slot-placeholder { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; padding: 12px; color: #555563; font-size: 14px; } .slot-visual { position: relative; flex: 1; min-height: 0; overflow: hidden; } .library-panel { width: clamp(320px, 30vw, 420px); min-width: 320px; flex-shrink: 0; display: flex; flex-direction: column; /* Show mode collapses this panel to width 0 rather than hiding it; without clipping, the tab strip and pane contents spill across the stage. */ overflow: hidden; background: #141418; border-left: 1px solid #2a2a32; transition: width 0.22s ease, min-width 0.22s ease, border-color 0.22s ease; } /* ---------- panel tabs ---------- */ .library-tabs { display: flex; flex-shrink: 0; gap: 4px; padding: 8px 8px 0; border-bottom: 1px solid #2a2a32; transition: opacity 0.18s ease; } .lib-tab { flex: 1; min-width: 0; padding: 9px 6px; font-family: inherit; font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #8a8a99; background: none; border: none; border-bottom: 2px solid transparent; border-radius: 6px 6px 0 0; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .lib-tab:hover { color: #c6cada; background: rgba(255, 255, 255, 0.03); } .lib-tab.is-active { color: #cfe0f7; border-bottom-color: #4a6fa5; background: rgba(74, 111, 165, 0.12); } /* One pane at a time. display:none rather than opacity, so a hidden pane cannot be clicked, tabbed into, or measured. */ .library-pane { display: none; min-height: 0; transition: opacity 0.18s ease; } .library-pane.is-active { /* This chain is load-bearing: .library-scroll's flex:1 only expands because every ancestor up to .library-panel is a column flex box with min-height:0. */ display: flex; flex: 1; flex-direction: column; min-height: 0; } .pane-scroll { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 10px; touch-action: pan-y; } .pane-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .pane-head-spaced { margin-top: 16px; padding-top: 12px; border-top: 1px solid #2a2a32; } .pane-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #9a9aa8; } .pane-foot { flex-shrink: 0; padding: 6px 10px 8px; border-top: 1px solid #2a2a32; } .slot-size-readout { font-size: 11px; line-height: 1.4; color: #7d8492; user-select: text; } .pco-controls { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; margin-top: 8px; } .pco-select { min-width: 0; width: 100%; padding: 7px 10px; border-radius: 8px; border: 1px solid #3a3a44; background: #25252e; color: #e8e8ec; font-size: 13px; } .pco-status { margin-top: 8px; min-height: 18px; font-size: 12px; color: #9ea7bc; } .pco-birthday-result { min-height: 0; line-height: 1.45; } .pco-birthday-result:empty { display: none; } /* ---- Birthday Mode settings ---- */ .bday-block { margin-top: 14px; border-top: 1px solid #23232c; padding-top: 12px; } .bday-head { display: flex; align-items: center; justify-content: space-between; } .bday-title { font-size: 13px; font-weight: 700; color: #e7e7ef; } .bday-switch { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: #9ea7bc; } .bday-controls { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; } .bday-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .bday-line-label { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: #8a8a99; min-width: 74px; } .bday-line-label em { font-style: normal; text-transform: none; letter-spacing: 0; color: #6d6d7a; } .seg-inline { display: inline-flex; gap: 4px; background: #0e0e14; border: 1px solid #23232c; border-radius: 9px; padding: 3px; } .seg-inline button { appearance: none; border: 0; background: transparent; color: #9ea7bc; font: inherit; font-size: 12px; font-weight: 600; padding: 5px 12px; border-radius: 6px; cursor: pointer; } .seg-inline button[aria-pressed="true"] { background: #f5c451; color: #241a02; } .bday-check { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: #c6cada; cursor: pointer; } .bday-test-select { flex: 1; min-width: 150px; } .bday-hint { font-size: 11px; color: #6d6d7a; } .pco-position-builder { display: none; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; margin-top: 8px; } .pco-position-list { display: none; flex-direction: column; gap: 6px; margin-top: 8px; max-height: 170px; overflow-y: auto; } .pco-position-empty { padding: 8px 10px; border: 1px dashed #353542; border-radius: 8px; color: #808092; font-size: 12px; } .pco-position-item { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto auto auto auto; gap: 6px; align-items: center; padding: 8px; border: 1px solid #30303a; border-radius: 8px; background: #1a1a20; } .pco-position-copy { min-width: 0; } .pco-position-name { font-size: 12px; font-weight: 600; color: #ececf1; } .pco-position-team { margin-top: 2px; font-size: 11px; color: #9da5b6; } .pco-count-chip { min-width: 34px; text-align: center; font-size: 12px; color: #d8deea; } .library-scroll { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 8px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; align-content: start; touch-action: pan-y; transition: opacity 0.18s ease; } .lib-item { position: relative; width: 100%; aspect-ratio: 3 / 4; border-radius: 10px; overflow: hidden; border: 2px solid #33333e; background: #0a0a0c; flex-shrink: 0; touch-action: pan-y; cursor: grab; } .lib-item:active { cursor: grabbing; } .lib-item.dragging-source { opacity: 0.35; } .lib-item.on-lineup { border-color: #3d5a80; box-shadow: 0 0 0 1px rgba(61, 90, 128, 0.5); } .lib-item img { width: 100%; height: 100%; aspect-ratio: 3 / 4; object-fit: cover; object-position: center top; display: block; pointer-events: none; } .lib-name-overlay { position: absolute; top: 8px; left: 8px; right: 8px; z-index: 1; padding: 7px 8px; border-radius: 8px; background: rgba(8, 8, 12, 0.74); color: #f4f4f6; font-size: 11px; font-weight: 700; line-height: 1.2; text-align: center; word-break: break-word; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65); pointer-events: none; } .lib-delete { position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; border: none; border-radius: 8px; background: rgba(0, 0, 0, 0.6); color: #fff; font-size: 14px; line-height: 1; cursor: pointer; z-index: 2; touch-action: manipulation; padding: 0; } .lib-delete:hover { background: #8b2942; } .tile { width: 100%; height: 100%; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; background: #1a1a22; position: relative; touch-action: none; cursor: grab; } .tile:active { cursor: grabbing; } .tile.dragging-source { opacity: 0.4; } .tile .photo-area { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: #0a0a0c; } .tile img { width: 100%; height: 100%; object-fit: contain; object-position: center center; pointer-events: none; display: block; } .tile .parallax-viewport { position: absolute; inset: 0; z-index: 1; overflow: hidden; pointer-events: none; } .tile .moving-stage-image { position: absolute; inset: -5%; width: 110%; height: 110%; object-fit: cover; object-position: center center; will-change: transform; backface-visibility: hidden; transform-origin: center center; image-rendering: -webkit-optimize-contrast; transform: translate3d(calc(var(--cam-x, 0px) * -0.3), calc(var(--cam-y, 0px) * -0.3), 0) scale(1.01); } .tile .static-stage-image { width: 100%; height: 100%; object-fit: cover; object-position: center center; } .tile .name-badge { position: absolute; top: 8px; left: 8px; max-width: calc(100% - 52px); padding: 6px 10px; border-radius: 8px; background: rgba(8, 8, 12, 0.84); color: #f4f4f6; font-size: clamp(11px, 1vw, 15px); font-weight: 700; line-height: 1.2; z-index: 3; cursor: pointer; touch-action: manipulation; word-break: break-word; } .empty-name-hotspot { position: absolute; top: 8px; left: 8px; right: 40px; min-height: 28px; border: 0; border-radius: 8px; padding: 6px 10px; background: transparent; color: #f4f4f6; font-size: clamp(11px, 1vw, 15px); font-weight: 700; line-height: 1.2; text-align: left; z-index: 2; cursor: pointer; touch-action: manipulation; } .empty-name-hotspot:not(:empty) { background: rgba(8, 8, 12, 0.68); } .empty-name-hotspot:focus-visible { outline: 2px solid #6b9ee0; outline-offset: 1px; } .tile .name-badge:empty::before { content: "Tap for name"; color: #b2b2c0; font-weight: 600; } .tile .caption { position: relative; padding: 10px 8px; font-size: clamp(11px, 1vw, 15px); font-weight: 600; text-align: center; background: rgba(12, 12, 16, 0.88); border-top: 1px solid #2e2e38; min-height: 42px; display: flex; align-items: center; justify-content: center; word-break: break-word; touch-action: manipulation; cursor: pointer; } .stage-slot > .caption:empty::before { content: "Tap for mic / pack"; color: #6b6b79; font-weight: 500; } /* ---- Birthday Mode tile effects ---- */ .tile.birthday-subtle { animation: bdayGlowSubtle 3.4s ease-in-out infinite; } .tile.birthday-full { animation: bdayGlowFull 2.2s ease-in-out infinite; } @keyframes bdayGlowSubtle { 0%, 100% { box-shadow: inset 0 0 0 2px rgba(245, 196, 81, 0.55), 0 0 14px rgba(245, 196, 81, 0.18); } 50% { box-shadow: inset 0 0 0 2px rgba(245, 196, 81, 0.85), 0 0 20px rgba(245, 196, 81, 0.32); } } @keyframes bdayGlowFull { 0%, 100% { box-shadow: inset 0 0 0 3px rgba(245, 196, 81, 0.8), 0 0 20px rgba(245, 196, 81, 0.42); } 50% { box-shadow: inset 0 0 0 3px #ffd873, 0 0 34px rgba(245, 196, 81, 0.66); } } /* Birthday Mode intentionally keeps animating even under OS "reduced motion" — it's a celebration the operator opts into, and freezing it leaves confetti stuck mid-fall. */ .birthday-confetti { position: absolute; inset: 0; z-index: 2; pointer-events: none; } .tile .remove { position: absolute; top: 6px; right: 6px; width: 30px; height: 30px; border: none; border-radius: 8px; background: rgba(0, 0, 0, 0.6); color: #fff; font-size: 18px; line-height: 1; cursor: pointer; z-index: 4; touch-action: manipulation; padding: 0; opacity: 0; pointer-events: none; transition: opacity 0.15s ease; } body:not(.chrome-hidden) .tile .remove { opacity: 1; pointer-events: auto; } .tile .remove:hover { background: #8b2942; } .stage-slot { display: flex; flex-direction: column; } .stage-slot > .caption { flex-shrink: 0; padding: 10px 8px; font-size: clamp(11px, 1vw, 15px); font-weight: 600; text-align: center; background: rgba(12, 12, 16, 0.88); border-top: 1px solid #2e2e38; min-height: 42px; display: flex; align-items: center; justify-content: center; word-break: break-word; touch-action: manipulation; cursor: pointer; } .empty { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; text-align: center; color: #666670; font-size: 16px; margin: 0; pointer-events: none; } .empty.visible { display: flex; } body.chrome-hidden .toolbar { max-height: 0; padding-top: 0; padding-bottom: 0; opacity: 0; transform: translateY(-10px); border-bottom-color: transparent; pointer-events: none; } body.chrome-hidden .library-panel { width: 0; min-width: 0; border-left-color: transparent; } body.chrome-hidden .library-tabs, body.chrome-hidden .library-pane { opacity: 0; pointer-events: none; } body.chrome-hidden .tile .remove { opacity: 0; pointer-events: none; } body.chrome-hidden .stage-slot.empty-slot::before, body.chrome-hidden .slot-placeholder, body.chrome-hidden .stage-slot > .caption:empty::before, body.chrome-hidden .tile .name-badge:empty::before { opacity: 0; } .drag-ghost { position: fixed; width: 88px; height: 118px; border-radius: 10px; overflow: hidden; border: 2px solid #4a6fa5; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55); pointer-events: none; z-index: 1000; opacity: 0.92; } .drag-ghost img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; } .drag-ghost.hidden { display: none; } .modal { position: fixed; inset: 0; z-index: 2000; display: flex; align-items: center; justify-content: center; padding: 16px; } .modal.hidden { display: none; } .modal-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.72); } .modal-card { position: relative; width: min(720px, 100%); max-height: min(92vh, 900px); overflow: auto; background: #1e1e26; border: 1px solid #3a3a48; border-radius: 16px; padding: 20px; box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5); } .modal-card-small { width: min(420px, 100%); } .modal-title { margin: 0 0 12px; font-size: 18px; font-weight: 600; } .modal-copy { margin: 0 0 12px; color: #aeb6c8; font-size: 13px; line-height: 1.4; } .modal-field { display: block; margin-bottom: 12px; color: #c6cada; font-size: 13px; } .modal-field span { display: block; margin-bottom: 6px; } .label-input { width: 100%; padding: 14px 12px; font-size: 22px; border-radius: 10px; border: 2px solid #4a6fa5; background: #121218; color: #e8e8ec; margin-bottom: 14px; } .label-input:focus { outline: none; border-color: #6b9ee0; } .modal-input { margin-bottom: 0; font-size: 16px; } .number-input { text-align: center; font-size: 32px; font-weight: 700; letter-spacing: 0.04em; } .vkeyboard { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; } .vk-row { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; } .vk-key { min-width: 44px; height: 48px; padding: 0 12px; border-radius: 8px; border: 1px solid #3a3a48; background: #2a2a34; color: #e8e8ec; font-size: 18px; font-weight: 600; cursor: pointer; touch-action: manipulation; } .vk-key:hover { background: #363642; } .vk-key.wide { min-width: 120px; } .vk-key.space { flex: 1; min-width: 160px; max-width: 320px; } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; } /* ---------- ProPresenter config (in library head) ---------- */ .pp-controls { margin-top: 10px; padding-top: 10px; border-top: 1px solid #2a2a32; } .pp-controls-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .pp-controls-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #9a9aa8; } .pp-controls-buttons { display: flex; gap: 8px; } .pp-status { margin-top: 8px; min-height: 18px; font-size: 12px; color: #9ea7bc; } .pp-playlist-select { margin-top: 8px; } /* ---------- Studio displays (hosting the Now/Next page on the LAN) ---------- */ .display-urls { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; /* A multi-homed machine can offer five or six addresses; keep the list from taking over the panel on its own. */ max-height: 168px; overflow-y: auto; touch-action: pan-y; } .display-urls:empty { display: none; } .display-url-row { display: flex; align-items: center; gap: 8px; padding: 7px 9px; border: 1px solid #2a2a34; border-radius: 8px; background: rgba(26, 26, 32, 0.7); } .display-url-row.is-best { border-color: #4a6fa5; background: rgba(74, 111, 165, 0.16); } .display-url-main { flex: 1; min-width: 0; } .display-url-text { font-size: 13px; font-weight: 700; color: #cfe0f7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; user-select: text; } .display-url-note { margin-top: 2px; font-size: 11px; line-height: 1.35; color: #8a8a99; } .display-url-row.is-best .display-url-note { color: #9fb6d6; } .display-port-row { display: flex; align-items: center; gap: 10px; margin-top: 8px; } .display-port-field { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #a0a0ae; } .display-port-field input { width: 74px; padding: 6px 8px; border-radius: 8px; border: 1px solid #33333d; background: #0f0f13; color: #e8e8ec; font-size: 13px; } .display-port-field input:focus { outline: none; border-color: #4a6fa5; } /* ---------- ProPresenter Now / Next column (show mode) ---------- */ .pp-column { display: none; width: clamp(280px, 26vw, 380px); min-width: 280px; flex-shrink: 0; flex-direction: column; gap: 14px; padding: 18px 16px; background: rgba(16, 16, 20, 0.96); border-left: 1px solid #2a2a32; overflow: hidden; } /* Only present in minimal/show mode, and only when ProPresenter has live data. */ body.chrome-hidden.pp-active .pp-column { display: flex; } .pp-playlist-name { font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #8a8a99; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pp-playlist-name:empty { display: none; } .pp-now { border: 1px solid #30303c; border-radius: 14px; background: linear-gradient(160deg, rgba(61, 90, 128, 0.22), rgba(26, 26, 34, 0.6)); padding: 14px 16px; } .pp-now-label { font-size: 11px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: #7fa8e0; } .pp-now-name { margin-top: 6px; /* Size is set dynamically by fitNowTitle() in renderer.js to fill the box; this is just the pre-fit default. Keep line-height in sync with that helper. */ font-size: 36px; font-weight: 700; line-height: 1.1; color: #f4f6fb; /* Wrapping is controlled by fitNowTitle() in renderer.js — it forbids mid-word breaks and sizes down until whole words fit, so the base must not force break-word. */ overflow-wrap: normal; word-break: normal; } .pp-now-meta { margin-top: 10px; font-size: clamp(13px, 1.2vw, 17px); font-weight: 600; color: #c6cada; font-variant-numeric: tabular-nums; /* Never wrap — a 2nd line would resize the title box and flap the fitted title size. */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pp-progress { margin-top: 10px; height: 6px; border-radius: 999px; background: rgba(255, 255, 255, 0.1); overflow: hidden; } .pp-progress-fill { height: 100%; border-radius: 999px; background: #4a6fa5; transition: width 0.25s ease; } .pp-next-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 8px; } .pp-section-label { font-size: 11px; font-weight: 800; letter-spacing: 0.14em; text-transform: uppercase; color: #7a7a8a; } .pp-next-list { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 8px; overflow: hidden; } .pp-next-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid #2a2a34; border-radius: 10px; background: rgba(26, 26, 32, 0.7); } .pp-next-name { flex: 1; min-width: 0; font-size: clamp(14px, 1.3vw, 19px); font-weight: 600; color: #e2e4ec; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pp-count-chip { flex-shrink: 0; min-width: 30px; padding: 3px 9px; text-align: center; border-radius: 999px; background: rgba(74, 111, 165, 0.28); color: #cfe0f7; font-size: 13px; font-weight: 700; } .pp-type-tag { flex-shrink: 0; font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: #8a8a99; } .pp-next-item.is-header { background: none; border: none; border-bottom: 1px solid #2f2f3a; border-radius: 0; padding: 10px 2px 4px; } .pp-next-item.is-header .pp-next-name { font-size: 11px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; color: #8a8a99; white-space: normal; } .pp-waiting { font-size: 14px; color: #7a7a8a; } .pp-error { margin-top: 6px; font-size: 11px; color: #d98c8c; word-break: break-word; }