/* Varaverk plugin styles — inherits unRAID theme, adds plugin-specific layout */ #varaverk-wrap { padding: 10px; font-family: inherit; } /* Tab bar Scrolls itself once the tabs stop fitting. Without this the row simply overflowed and the whole page went with it, so reaching the last tab on a phone meant dragging the entire layout sideways — every card, not just the bar. The scrollbar is hidden the way the log panes hide theirs: a visible one costs a row of height on a bar that is 30px tall, and the content is obviously draggable. overscroll-behavior stops a flick at either end of the bar from chaining out and scrolling the page instead. */ #vv-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid #444; align-items: flex-end; overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; scrollbar-width: none; } #vv-tabs::-webkit-scrollbar { display: none; } /* Neither squashed nor broken across lines — a flex item's default is to shrink, which turned "Partnership" into three characters and an ellipsis before the bar ever decided to scroll. */ .vv-tab { padding: 6px 16px; text-decoration: none; color: #aaa; border-radius: 4px 4px 0 0; flex-shrink: 0; white-space: nowrap; } .vv-tab:hover { color: #fff; background: #333; } .vv-tab.active { color: #fff; background: #555; border-bottom: 2px solid #fff; } /* Expand toggle button */ #vv-expand-btn { background: none; border: none; cursor: pointer; color: #333; font-size: 15px; padding: 2px 8px 4px; line-height: 1; border-radius: 3px; transition: color .15s; margin-left: 6px; flex-shrink: 0; } #vv-expand-btn:hover { color: #888; } #vv-expand-btn.active { color: #aaa; } /* Fullscreen mode — hide Unraid chrome, reclaim the space */ body.vv-fullscreen #header { display: none !important; } body.vv-fullscreen #menu { display: none !important; } body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5rem !important; } /* Cards / layout */ .vv-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; } /* One card surface for the whole plugin. #161616 on #2a2a2a was already what Settings, Watchdog, Arrs and Rsync drew — four pages agreeing under four different class names — while this shared one was still the older #1e1e1e on #444. Bringing it here is what makes them one look rather than two, and it is done on the appearance alone: flex and min-width stay, because the Monitor grid needs a 200px floor and the Arrs grid needs min-width:0, and merging those would change how both size their columns to fix a colour. */ .vv-card { flex: 1; min-width: 200px; background: #161616; border: 1px solid #2a2a2a; border-radius: 6px; padding: 12px; } .vv-wide { flex: 100%; } .vv-card h3 { margin: 0 0 10px; font-size: 13px; text-transform: uppercase; color: #888; letter-spacing: 0.05em; white-space: normal; overflow: hidden; min-width: 0; display: flex; align-items: center; justify-content: space-between; } /* Cog icon — links to related Unraid page. Hidden until card hovered. */ .vv-card-cog { color: #2a2a2a; font-size: 13px; line-height: 1; text-decoration: none; padding: 1px 3px; border-radius: 3px; flex-shrink: 0; transition: color 0.15s, background 0.15s; font-style: normal; font-weight: normal; text-transform: none; letter-spacing: 0; cursor: pointer; } .vv-card:hover .vv-card-cog { color: #4a4a4a; } .vv-card-cog:hover { color: #aaa !important; background: #333; } /* Card header icon wrapper */ .vv-ico { display:inline-flex; align-items:center; opacity:0.38; flex-shrink:0; } /* Status accent — left border colour for state-driven cards */ .vv-accent-ok { border-left-color: #2a5a2a !important; } .vv-accent-warn { border-left-color: #5a4000 !important; } .vv-accent-err { border-left-color: #5a1e1e !important; } #vv-monitor .vv-card { transition: border-left-color 0.5s; } /* Status banner — coloured strip at top of card body */ .vv-banner { border-radius: 4px; padding: 5px 10px; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; font-size: 12px; font-weight: 600; } .vv-banner-ok { background: #061306; border: 1px solid #1a401a; color: #4caf50; } .vv-banner-warn { background: #130e00; border: 1px solid #3d2e00; color: #ff9800; } .vv-banner-err { background: #140404; border: 1px solid #3d1010; color: #f44336; } .vv-banner-off { background: #0d0d0d; border: 1px solid #252525; color: #555; } /* System card — no h3, no top padding waste */ #vv-system { padding-top: 14px; } /* System action buttons */ .vv-sys-btn { background: #2a2a2a; border: 1px solid #e65100; color: #ff9800; border-radius: 3px; padding: 3px 0; font-size: 6px; cursor: pointer; line-height: 1; width: 50px; min-width: 0; text-align: center; } .vv-sys-btn:hover { background: #3a2000; color: #ffb74d; border-color: #ff9800; } /* Fallback state badge */ .vv-state-badge { font-size: 20px; font-weight: bold; padding: 4px 0; margin-bottom: 2px; } .vv-state-normal { color: #4caf50; } .vv-state-failover { color: #f44336; } .vv-state-no_internet { color: #ff9800; } .vv-state-dark { color: #9e9e9e; } .vv-state-unknown { color: #666; } /* Docker table */ #vv-docker-table { width: 100%; border-collapse: collapse; font-size: 13px; } #vv-docker-table th { text-align: left; padding: 4px 8px; color: #888; border-bottom: 1px solid #444; } #vv-docker-table td { padding: 4px 8px; border-bottom: 1px solid #2a2a2a; } .vv-status-up { color: #4caf50; } .vv-status-down { color: #f44336; } /* Scheduler */ .vv-hint { color: #888; font-size: 13px; margin-bottom: 16px; } .vv-sched-card { margin-bottom: 10px; } .vv-script { background: #161616; border: 1px solid #333; border-radius: 4px; padding: 6px 10px; margin: 4px 0; margin-left: 20px; } .vv-job-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; transition: background 0.1s, box-shadow 0.1s; } /* Orchestrator top row — clickable, show pointer + hover feedback */ .vv-orch-row { cursor: pointer; border-radius: 4px; } .vv-orch-row:hover { background: rgba(255,255,255,0.04); } .vv-job-label { flex: 1; font-size: 16px; font-weight: bold; color: #6fcf97; min-width: 80px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .vv-job-actions { display: flex; align-items: center; gap: 8px; padding-left: 44px; margin-top: 6px; } .vv-job-desc { font-size: 14px; color: #777; margin: 3px 0 2px 0; padding-left: 44px; box-sizing: border-box; width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; } .vv-cron { flex: 0 0 80px; width: 80px; background: #111; border: 1px solid #444; color: #ddd; padding: 4px 6px; border-radius: 4px; font-family: monospace; font-size: 11px; } .vv-event-badge { flex: 0 0 auto; padding: 3px 8px; border-radius: 4px; font-size: 12px; background: #1a3a1a; border: 1px solid #2e6b2e; color: #6fcf6f; white-space: nowrap; font-weight: 500; } .vv-flag-badge { flex: 0 0 auto; padding: 2px 6px; border-radius: 3px; font-size: 11px; background: #2e2200; border: 1px solid #6b4e00; color: #d4a017; white-space: nowrap; font-family: monospace; } .vv-log-label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #888; cursor: pointer; white-space: nowrap; flex-shrink: 0; } .vv-log-label input { cursor: pointer; accent-color: #4caf50; } .vv-log-label:has(input:checked) { color: #4caf50; } .vv-children { padding-top: 8px; border-top: 1px solid #333; margin-top: 8px; } .vv-advanced-toggle { background: none; border: 1px solid #555; color: #aaa; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; } .vv-advanced-toggle:hover { border-color: #888; color: #fff; } .vv-btn-sm { padding: 3px 10px; background: #2a2a2a; border: 1px solid #555; color: #ccc; border-radius: 4px; cursor: pointer; font-size: 12px; white-space: nowrap; } .vv-btn-sm:hover { border-color: #888; color: #fff; } .vv-btn-sm.active { border-color: #4caf50; color: #4caf50; } /* Save checkmark */ .vv-save-check { color: #4caf50; font-size: 13px; width: 14px; flex-shrink: 0; opacity: 0; text-align: center; } @keyframes vv-check-fade { 0%,60% { opacity: 1; } 100% { opacity: 0; } } .vv-save-check.vv-check-show { animation: vv-check-fade 2s forwards; } /* Running dot */ .vv-job-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; } .vv-dot-running { background: #4caf50; animation: vv-pulse-dot 1s ease-in-out infinite; } #vv-log-dot { animation: vv-pulse-dot 1s ease-in-out infinite; } @keyframes vv-pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.2; } } /* Selected job row — editor-style left accent + blue tint */ .vv-row-selected { background: rgba(100,149,237,0.1); border-radius: 4px; outline: 1px solid rgba(100,149,237,0.35); box-shadow: inset 3px 0 0 #6495ed; } /* Two-panel layout */ #vv-sched-layout { display: flex; gap: 16px; align-items: stretch; } #vv-sched-left { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-self: flex-start; } #vv-sched-cards { flex: 1; } #vv-sched-right { display: none; flex: 1 1 0; min-width: 0; flex-direction: column; overflow: hidden; align-self: flex-start; } /* Flex items default to min-height:auto, which resolves to their content size — so a view with a long log in it refuses to shrink. The assistant below it is flex-shrink:0 and this panel is a fixed height with overflow:hidden, so whatever will not fit is not scrolled, it is clipped off the bottom: the assistant's own composer, which is the last thing in the panel. vvFitRight() sizes these explicitly and this is what lets that size actually take effect. */ #vv-sched-right > * { min-height: 0; } #vv-sched-right.vv-panel-visible { display: flex; } .vv-log-card { flex: 1; display: flex; flex-direction: column; padding-bottom: 0; } .vv-log-right-pre { max-height: none; overflow-y: auto; } /* Scheduler stacked layout (narrow viewport) */ @media (max-width: 900px) { #vv-sched-layout { flex-direction: column; align-items: stretch; } #vv-sched-left { flex: none; width: 100%; } #vv-sched-right { flex-direction: column; width: 100%; overflow: visible; } .vv-log-right-pre { min-height: 520px; max-height: 680px; } /* Toolbar: stack title row above buttons row, let buttons wrap */ .vv-log-toolbar { flex-direction: column; align-items: flex-start; gap: 6px; } .vv-log-toolbar > div { flex-wrap: wrap; gap: 6px !important; } /* Array-event rows: badge already shows the event, cron input is redundant */ .vv-job-row:has(.vv-event-badge) .vv-cron { display: none; } } /* Plugin settings row (Advanced mode) */ .vv-nb-settings { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 5px 10px; background: #101010; border-bottom: 1px solid #1a1a1a; } /* ── Monitor: locked row height + scrollable cards ────────────────────────── */ /* Cards on the monitor grid are flex columns — h3 pins, body scrolls */ #vv-monitor .vv-card { display: flex; flex-direction: column; overflow: hidden; } #vv-monitor .vv-card h3 { flex-shrink: 0; } #vv-monitor .vv-card > div { flex: 1; min-height: 0; overflow-y: auto; -ms-overflow-style: none; scrollbar-width: none; } #vv-monitor .vv-card > div::-webkit-scrollbar { display: none; } /* Hide scrollbars on any nested scrollable div inside monitor cards */ #vv-monitor .vv-card div::-webkit-scrollbar { display: none; } /* Dynamic row heights capped per screen tier — rows size to content, never exceed the cap. minmax(0, Xpx): track is content-driven but capped; align-items:stretch makes all cards in a row fill the track, so short cards (Pools, Watchdog) match tall ones (Array). Breakpoints are viewport height (after browser chrome), not screen height. */ /* ~720p (viewport ≤ 700px) */ @media (min-width: 481px) and (max-height: 700px) { #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 160px) / 4)); } } /* ~1080p (viewport 701–1100px) */ @media (min-width: 481px) and (min-height: 701px) and (max-height: 1100px) { #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 240px) / 4)); } } /* ~1440p (viewport 1101–1450px) — calibrated on 15" 1440p display */ @media (min-width: 481px) and (min-height: 1101px) and (max-height: 1450px) { #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 335px) / 4)); } } /* ~4K (viewport > 1450px) */ @media (min-width: 481px) and (min-height: 1451px) { #vv-monitor { grid-auto-rows: minmax(0, calc((100vh - 500px) / 4)); } } /* Mobile: natural heights, let page scroll */ @media (max-width: 480px) { #vv-monitor { grid-auto-rows: auto !important; } #vv-monitor .vv-card { overflow: visible !important; } #vv-monitor .vv-card > div { overflow-y: visible; min-height: auto; } } /* ── Monitor: AI row ────────────────────────────────────────────────────────── Same eight columns and the same gap as #vv-monitor, and margin-top matches that grid's row gap, so the two containers read as one board. What differs is height, which is the whole reason it is a separate grid: no grid-auto-rows cap and no overflow:hidden on the cards, so the Assistant's expand control gives the transcript real room instead of growing it inside a box the row had already sized. align-items:start, not the stretch the grid above uses, so a card opts in to matching the Assistant rather than being dragged to its height. All three in this row opt in below; the /* ── Monitor column ladder ──────────────────────────────────────────────────────────────────── The grid lives here rather than in a style="" on the div, which is why nothing below needs !important any more. An inline style outranks every stylesheet rule, so the old overrides had to shout to be heard; moving the declaration into the cascade lets the ladder simply cascade. BREAKPOINTS ARE DERIVED, NOT CHOSEN. A column count is viable exactly when the cards still fit: window >= N * card-floor + (N-1) * gap + wrap-padding = N * 200 + (N-1) * 12 + 20 which gives 12:2552 10:2128 8:1704 6:1280 4:856 2:432 1:220. Every value below is that arithmetic minus one, never a number picked by eye. If .vv-card's 200px min-width ever changes, these must be recomputed — they are a function of it, not independent settings. This replaces a single 8 -> 4 step at 1024px, which left a 679px dead zone (1025-1704) where eight columns were forced into a box that could not hold them. A 1327px tablet in portrait overflowed by 377px — the "scroll sideways to see the last fifth" bug. minmax(0, 1fr), not 1fr: bare 1fr means minmax(auto, 1fr), and that auto floor resolves to the larger of min-content and min-width. One card holding an unbreakable string would otherwise push its column past its share and drag the whole grid out of the container. The row axis has carried this guard for a long time (grid-auto-rows: minmax(0, ...)); the column axis never did. */ #vv-monitor { --vv-cols: 12; display: grid; grid-template-columns: repeat(var(--vv-cols), minmax(0, 1fr)); gap: 12px; width: 100%; box-sizing: border-box; } @media (max-width: 2551px) { #vv-monitor { --vv-cols: 10; } } @media (max-width: 2127px) { #vv-monitor { --vv-cols: 8; } } @media (max-width: 1703px) { #vv-monitor { --vv-cols: 6; } } @media (max-width: 1279px) { #vv-monitor { --vv-cols: 4; } } @media (max-width: 855px) { #vv-monitor { --vv-cols: 2; } } @media (max-width: 431px) { #vv-monitor { --vv-cols: 1; } } /* Spans survive the ladder unchanged from 12 columns down to 4. A span-4 card is half the board at eight columns and a third at twelve, and that is the point: the cards keep their size while more of them fit per row, which is the density the wide screens are for. Only the bottom two rungs need help, because there a span can exceed the whole grid. */ @media (max-width: 855px) { /* Only two cards exceed a 2-column grid. Named explicitly rather than matched on their inline style text, which would break the moment a span is edited or reordered. */ #vv-docker-folders, #vv-streams { grid-column: span 2; } } /* One column: any span >1 would make the grid invent an implicit column and reintroduce the horizontal scroll this whole ladder exists to remove. After the 2-col clamp above, deliberately — equal specificity, so source order decides. */ @media (max-width: 431px) { #vv-monitor > .vv-card { grid-column: 1 / -1; } } /* Height, not width, is what makes a phone in landscape unusable. The escape hatch below was keyed to max-width:480 only, so portrait (393px wide) got content-sized rows and worked, while landscape (851x393) missed the hatch and inherited the four-row cap: (393-160)/4 = 58px cards, overflow:hidden, scrollbars disabled. Nothing was visible and nothing said so. Four capped rows can never be usable below ~700px of viewport, so the cap is dropped and the page scrolls instead. Everything actually in use stays capped: 1080p full is 210px rows, the tablet in landscape 263px, in portrait 405px. */ @media (max-width: 855px), (max-height: 700px) { #vv-monitor { grid-auto-rows: auto; } #vv-monitor .vv-card { overflow: visible; } #vv-monitor .vv-card > div { overflow-y: visible; min-height: auto; } } default stays start so a card added later has to say so. */ #vv-monitor-ai { display: grid; grid-template-columns: repeat(8, 1fr); gap: 12px; margin-top: 12px; width: 100%; box-sizing: border-box; align-items: start; } /* Explicit placement, two rows. The left column carries AI over Tokens; Conversations and the Assistant span both rows, so the Assistant sets the height and the two stacked cards divide what it sets. This has to be written as row+column rather than the spans that were inline on the markup: a span says how wide, never which row, and auto-placement would drop Tokens into the first gap it found rather than under AI. */ /* AI fills its own row rather than sitting at the top of it. The Assistant spans both rows, and grid hands a spanning item's surplus height to the tracks it spans — so at Large, row 1 grows well past what the AI card's few lines need and the card left a gap above Tokens. Stretching is a no-op at the smaller sizes, where there is no surplus to take. */ #vv-ai-stats-card { grid-column: 1 / span 1; grid-row: 1; align-self: stretch; } #vv-ai-tokens-card { grid-column: 1 / span 1; grid-row: 2; } #vv-ai-chats-card { grid-column: 2 / span 2; grid-row: 1 / span 2; } #vv-ai-assistant-card { grid-column: 4 / span 5; grid-row: 1 / span 2; } /* Conversations and Tokens take their height from the Assistant rather than setting their own. align-self overrides the row's align-items:start for these two, so they fill the track the Assistant defines — level at rest, and level again after expanding, since the track grows with it. They need their own scroll to do that, which Conversations did not need inside #vv-monitor: there it inherited a cap from `.vv-card > div { flex:1; overflow-y:auto }`. Out here both are content-driven, so forty saved chats — or a ledger with both hosts and a footer — would make the card the tallest thing in its column and set the height it is supposed to be following. The h3 pins and the body takes what is left, the same shape the monitor grid above uses. align-self alone does not finish the job, which is why vvAiRowSync() in monitor.php sets a max-height on both. A stretched item's own content still feeds grid track sizing — the track is sized from content first and stretched afterwards — so a card long enough grows the row it was meant to fit inside, and stretching to that row then changes nothing. The cap is measured from the Assistant, which is the one card here whose height is its own business. */ #vv-monitor-ai #vv-ai-chats-card, #vv-monitor-ai #vv-ai-tokens-card { align-self: stretch; display: flex; flex-direction: column; overflow: hidden; } #vv-monitor-ai #vv-ai-chats-card h3, #vv-monitor-ai #vv-ai-tokens-card h3 { flex-shrink: 0; } /* height:0 with flex:1 is the part that makes these follow rather than lead. flex-basis is already 0, so it changes nothing about the layout; what it changes is the height the body reports upward. Without it the card can still hand the auto-sized grid track a max-content height of its whole contents, and the track it was meant to fit inside grows to fit it instead. It grows back to fill the track through flex, then scrolls. */ #vv-monitor-ai #vv-ai-chats-card > div, #vv-monitor-ai #vv-ai-tokens-card > div { flex: 1; height: 0; min-height: 0; overflow-y: auto; -ms-overflow-style: none; scrollbar-width: none; } #vv-monitor-ai #vv-ai-chats-card > div::-webkit-scrollbar, #vv-monitor-ai #vv-ai-tokens-card > div::-webkit-scrollbar { display: none; } /* ── AI token ledger — shared by the AI tab and the Monitor card ────────────── Moved out of pages/ai.php so both surfaces can draw the same rows. A tab's inline