@@ -1,17 +1,16 @@
< script lang = "ts" >
import BackLink from '$ui/components/BackLink.svelte' ;
import { contextHref } from '$lib/navigation-context.js' ;
import { onMount , untrack } from 'svelte' ;
import { beforeNavigate , afterNavigate , goto } from '$app/navigation' ;
import { page } from '$app/state' ;
import type { CapacityData , CapacityGame , CapacityQuery , CapacityRange } from '$lib/capacity.js' ;
import { hostReadinessLabel } from '$lib/hosts.js' ;
import { pollVisible } from '$ui/poll.js' ;
import { timeFormat } from '$ui/stores/time-zone.js' ;
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte' ;
import Button from '$ui/components/primitives/Button.svelte' ;
import Field from '$ui/components/primitives/Field.svelte' ;
import Select from '$ui/components/primitives/Select.svelte' ;
import CapacityHosts from '$ui/components/capacity/CapacityHosts.svelte' ;
import CapacityChart from '$ui/components/capacity/CapacityChart.svelte' ;
let { data } : { data : CapacityData } = $props ();
@@ -75,71 +74,70 @@
< svelte:head >< title > Capacity · Electric Cherry</ title >< meta name = "description" content = "Available game-server capacity, resource history, and host limits." /></ svelte:head >
< div class = "page" aria-busy = { navigating } >
< SectionHeader title = "Capacity" subtitle = "See what your hosts can run, what is limiting them, and how headroom changes." >
{ # snippet actions ()} < Button href = "/hosts" > Manage hosts</ Button >< Button disabled = { refreshing || navigating } onclick= {() => void refresh ()} > { refreshing ? 'Refreshing…' : 'Refresh' } </ Button > { /snippet }
</ SectionHeader >
< SectionHeader title = "Capacity" / >
{ #if page . url . searchParams . has ( 'returnTo' )} < BackLink fallback = "/hosts" /> { /if }
{ #if refreshError || navigationError } < div class = "refresh-error" role = "alert" >< strong > { navigationError || refreshError } </ strong >< span > Showing the last successfully loaded capacity report.</ span ></ div > { /if }
< div class = "toolbar" >
< div class = "filters" >< Field label = "Host" >< Select value = { report . query . hostId ?? '' } options= { hostOptions } disabled = { navigating } onchange= { event => void changeScope ({ hostId : ( event . currentTarget as HTMLSelectElement ). value || null })} /></ Field >
< Field label = "Launch game" >< Select value = { report . query . game } options= { gameOptions } disabled = { navigating } onchange= { event => void changeScope ({ game : ( event . currentTarget as HTMLSelectElement ). value as CapacityGame })} /></ Field ></ div >
< nav class = "ranges" aria-label = "History range" > { #each ranges as range } < a href = { scopeUrl ({ range : range.value })} aria-current= { report . query . range === range . value ? 'page' : undefined } data-sveltekit-keepfocus data-sveltekit-noscroll > { range . label } </ a > { /each } </ nav >
< div class = "freshness" >< span > { report . current . updatedAt ? `Updated $ { $timeFormat . minuteTime ( report . current . updatedAt )} ` : 'Waiting for host reports'}</span><Button variant="ghost" size="sm" disabled={refreshing || navigating} onclick={() => void refresh()}>{refreshing ? 'Refreshing…' : 'Refresh'}</Button></div>
</div>
<section class="overview" aria-labelledby="capacity-now-title">
< div class = "estimate" >< div class = "eyebrow" id = "capacity-now-title" > Available now · { scopeName } </ div >< div class = "estimate-number" > { count ( summary . estimatedServers )} < span > additional servers</ span ></ div >
< p > { summary . estimatedServers === null ? 'Waiting for enough host information to estimate capacity.' : `Estimated to fit across $ { summary . eligibleHosts } eligible $ { summary . eligibleHosts === 1 ? 'host' : 'hosts' }. ` } { report . query . game === 'all' ? 'Hosts ready for either T5 or T6 are counted once.' : `For $ { report . query . game === 't5' ? 'Black Ops I' : 'Black Ops II' } launches . ` } </ p ></ div >
< div class = "queue-context" >< a href = "/provisioning" >< strong > { count ( summary . queued )} </ strong >< span > servers queued < span aria-hidden = "true" > →</ span ></ span ></ a >< p > Across the fleet. Launches wait for capacity and startup slots automatically.</ p ></ div >
<div class="overview-heading"><div>
<h2 id="capacity-now-title">{#if summary.estimatedServers === null}Capacity estimate unavailable{:else}Estimated room for <strong>{count(summary.estimatedServers)}</strong> more {summary.estimatedServers === 1 ? 'instance' : 'instances'}{/if}</h2>
<p>{scopeName} · {summary.eligibleHosts} eligible {summary.eligibleHosts === 1 ? 'host' : 'hosts'}{report.query.game === 'all' ? '' : ` · $ { report . query . game === 't5' ? 'Black Ops I' : 'Black Ops II' } `}</p>
</div><div class="queue-context">
{#if summary.queued !== null && summary.queued > 0}<a href="/provisioning"><strong>{count(summary.queued)} {summary.queued === 1 ? 'instance' : 'instances'} queued →</strong></a>
{:else}<span>{summary.queued === null ? 'Queue unavailable' : 'No queued instances'}</span>{/if}<small>Fleet-wide</small>
</div></div>
<dl class="resources" aria-label="Current resource headroom">
<div><dt>CPU available</dt><dd>{count(summary.cpuCoresAvailable)} cores</dd></div>
<div><dt>Memory available</dt><dd>{memory(summary.memoryBytesAvailable)} GiB</dd></div>
<div><dt>Free ports</dt><dd>{count(summary.portsAvailable)}</dd></div>
<div><dt>Startup slots · Fleet-wide</dt><dd>{summary.startupSlots === null ? 'Unavailable' : ` $ { count ( summary . startupSlots )} $ { summary . startupLimit === null ? '' : ` of $ { summary . startupLimit } `} available` }</ dd ></ div >
</ dl >
</ section >
< div class = "resources" aria-label = "Current resource headroom" >
< article >< span class = "resource-label" > CPU available</ span >< div class = "resource-value" > { count ( summary . cpuCoresAvailable )} < small > cores</ small ></ div >< p > After OS reserve and pending starts</ p ></ article >
< article >< span class = "resource-label" > Memory available</ span >< div class = "resource-value" > { memory ( summary . memoryBytesAvailable )} < small > GiB</ small ></ div >< p > After OS reserve and pending starts</ p ></ article >
< article >< span class = "resource-label" > Ports available</ span >< div class = "resource-value" > { count ( summary . portsAvailable )} < small > ports</ small ></ div >< p > Free game ports on eligible hosts</ p ></ article >
< article >< span class = "resource-label" > Startup slots</ span >< div class = "resource-value" > { count ( summary . startupSlots )} < small > { summary . startupLimit === null ? 'available' : `/ $ { summary . startupLimit } `}</small></div><p>{count(summary.activeStartups)} starting · controller-wide</p></article>
</div>
<div class="freshness"><span>{report.current.updatedAt ? ` Capacity updated $ { $timeFormat . dateTime ( report . current . updatedAt )} ` : 'Waiting for host reports'}</span><span>Refreshes every 30 seconds while visible</span></div>
{ #if report . current . warnings . length }< aside class = "warnings" aria-label = "Capacity notices" >{ #each report . current . warnings as warning }< p >{ warning }</ p >{ /each }</ aside >{ /if }
<CapacityChart history={report.history} scope={scopeName} />
< section class = "hosts" aria-labelledby = "host-capacity-title" >
<div class= "section-heading"><div><h2 id= "host-capacity-title">Host capacity</h2><p>Find the resource that limits the next launch.</p></div><span>{summary.eligibleHosts} / {summary.totalHosts} eligible</span></div>
< div class = "section-heading" >< h2 id = "host-capacity-title" > Host capacity </ h2 >< span >{ summary . eligibleHosts } / { summary . totalHosts } eligible </ span ></ div >
{ #if ! report . current . hosts . length }< div class = "empty-hosts" >< h3 >{ report . query . hostId ? 'This host is unavailable' : 'No hosts yet' }</ h3 >< p >{ report . query . hostId ? 'Choose another host to view its capacity.' : 'Connect a host to start seeing capacity and history.' }</ p >< Button href = "/hosts" > Open hosts </ Button ></ div >
{:else}<div class="host-grid">{#each report.current.hosts as host (host.id)}<article class="host-card">
<div class="host-title"><h3><a href={contextHref(` /hosts/ $ { host . id } `, page.url)}>{host.name} <span aria-hidden="true">↗</span></a></h3><span class="status" class:eligible={host.eligible}>{!host.connected ? 'Offline' : hostReadinessLabel(host.readiness)}</span></div>
<div class="host-estimate"><strong>{count(host.estimatedServers)}</strong><span>additional servers{#if host.eligible && host.limitingResource}<small>Limited by {host.limitingResource.toLowerCase()}</small>{/if}</span></div>
<div class="game-tags">{#if host.games.length}{#each host.games as game}<span>{game === 't5' ? 'Black Ops I' : 'Black Ops II'}</span>{/each}{:else}<span>No games ready</span>{/if}</div>
<dl class="host-resources"><div><dt>CPU</dt><dd>{count(host.cpuCoresAvailable)} <small>cores</small></dd><span>{count(host.limits.cpu)} servers fit</span></div><div><dt>Memory</dt><dd>{memory(host.memoryBytesAvailable)} <small>GiB</small></dd><span>{count(host.limits.memory)} servers fit</span></div><div><dt>Ports</dt><dd>{host.portsAvailable} <small>/ {host.totalPorts}</small></dd><span>{host.limits.ports} servers fit</span></div></dl>
{#if host.reasons.length}<ul class="host-reasons">{#each host.reasons as reason}<li>{reason}</li>{/each}</ul>{/if}
<details class="allowances"><summary>Per-server resource estimates</summary>
<div class="allowance-grid">{#each [{ label: 'Running', value: host.calibration.running }, { label: 'Starting', value: host.calibration.startup }] as allowance}<div><strong>{allowance.label}</strong><p>{count(allowance.value.cpuCores)} cores · {memory(allowance.value.memoryBytes)} GiB</p><span>{allowance.value.source === 'bootstrap' ? 'Default allowance' : ` $ { allowance . value . source === 'host' ? 'Host' : 'Fleet' } observations · $ { allowance . value . samples } samples ` } </ span ></ div > { /each } </ div >
< p > Starts may need more headroom than running servers. Provisioning checks actual capacity before reserving a host and port.</ p >
</ details >
< p class = "host-updated" > { host . updatedAt ? `Reported $ { $timeFormat . dateTime ( host . updatedAt )} ` : 'No host report yet' } </ p >
</ article > { /each } </ div > { /if }
{ : else }< CapacityHosts hosts = { report . current . hosts } returnTo = { page . url . pathname + page . url . search } /> { /if }
</ section >
< details class = "methodology" >< summary > How capacity is estimated </ summary >< p > Additional servers are estimated from each eligible host’ s available CPU, memory, and unreserved ports, using its learned running-server allowance. The smallest resource allowance sets that host’ s limit. Shared hosts are counted once. </ p >< p > This is a planning estimate. Queued launches do not reserve capacity, and starting servers may require a larger allowance. Actual placement also checks current host readiness, game files, runtime, and available startup slots. </ p >< p > The timeline shows measured host resource headroom after the operating-system reserve. It does not reconstruct historical launch eligibility, queued jobs, or port availability. </ p ></ details >
< details class = "methodology" >< summary > Capacity details </ summary >
< p > Additional instances are estimated from each eligible host ’ s available CPU , memory , and unreserved ports , using its learned running - instance allowance . The smallest resource allowance sets that host ’ s limit . With All games selected , hosts ready for either T5 or T6 are counted once .</ p >
< p > CPU and memory availability account for the operating - system reserve and pending starts . Free ports are counted on eligible hosts .</ p >
< p > This is a planning estimate . Queued launches do not reserve capacity , and starting instances may require a larger allowance . Placement also checks current host readiness , game files , runtime , and startup slots .</ p >
< p >{ count ( summary . activeStartups )} instances starting across the fleet . Launches wait for capacity and startup slots automatically .</ p >
< p >{ report . current . updatedAt ? `Report updated $ { $timeFormat . dateTime ( report . current . updatedAt )}. ` : 'Waiting for host reports.' } Refreshes every 30 seconds while visible .</ p >
</ details >
< CapacityChart history = { report . history } scope = { scopeName } >
{ # snippet controls ()}< nav class = "ranges" aria-label = "History range" >{ #each ranges as range }< a href = { scopeUrl ({ range : range.value })} aria - current = { report . query . range === range . value ? 'page' : undefined } data - sveltekit - keepfocus data - sveltekit - noscroll > { range . label }</ a >{ /each }</ nav >{ /snippet }
</ CapacityChart >
</ div >
< style >
. page { display : grid ; gap : var ( -- space -5 ); min-width : 0 ; } . toolbar { display : flex ; align-items : end ; justify-content : space-between ; flex-wrap : wrap ; gap : var ( -- space -4 ) ; }
. filters { display : grid ; grid-template-columns : minmax ( 180 px , 260 px ) minmax ( 180 px , 260 px ); gap : var ( -- space -3 ) ; }
. ranges { display : flex ; gap : 3 px ; padding : 4 px ; border : 1 px solid var ( -- border - subtle ); border-radius : var ( -- radius - md ); background : var ( -- surface ) ; }
. ranges a { padding : 9 px 13 px ; border-radius : var ( -- radius - sm ); text-decoration : none ; font-size : var ( -- text - xs ); color : var ( -- text - secondary ); white-space : nowrap ; }
. ranges a [ aria-current ] { color : var ( -- text - primary ); background : var ( -- surface - raised ); } . ranges a : focus-visible { outline : 2 px solid var ( -- accent - primary ); outline-offset : 2 px ; }
. overview { display : flex ; justify-content : space-between ; align-items : center ; gap : var ( -- space -8 ); background : var ( -- surface ); border : 1 px solid var ( -- border - subtle ); border-left : 3 px solid var ( -- accent - primary ); border-radius : var ( -- radius - lg ); padding : var ( -- space -6 ) var ( -- space -8 ); }
. eyebrow { font-size : var ( -- text - xs ); color : var ( -- text - secondary ); font-weight : var ( -- weight - medium ); } . estimate-number { display : flex ; flex-wrap : wrap ; align-items : baseline ; column-gap : var ( -- space -4 ); margin-top : var ( -- space -3 ); font-size : 56 px ; font-weight : var ( -- weight - semibold ); line-height : 1.1 ; font-variant-numeric : tabular-nums ; letter-spacing : -.035 em ; }
. estimate-number span { font-size : var ( -- text - md ); font-weight : var ( -- weight - regular ); color : var ( -- text - secondary ); letter-spacing : normal ; } . estimate p , . queue-context p { color : var ( -- text - tertiary ); font-size : var ( -- text - xs ); line-height : 1.6 ; margin : var ( -- space -3 ) 0 0 ; }
. queue-context { flex : 0 0 210 px ; border-left : 1 px solid var ( -- border - strong ); padding-left : var ( -- space -6 ); } . queue-context a { color : var ( -- text - primary ); text-decoration : none ; display : flex ; align-items : baseline ; gap : var ( -- space -3 ); } . queue-context a : hover span { color : var ( -- accent - primary - hover ); } . queue-context strong { font-size : var ( -- text -2 xl ); font-weight : var ( -- weight - medium ); font-variant-numeric : tabular-nums ; } . queue-context span { color : var ( -- text - secondary ); font-size : var ( -- text - xs ); }
. resources { display : grid ; grid-template-columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : var ( -- space -3 ); } . resources article { background : var ( -- surface ); padding : var ( -- space -5 ); border : 1 px solid var ( -- border - subtle ); border-radius : var ( -- radius - md ); }
. resource-label { color : var ( -- text - secondary ); font-size : var ( -- text - xs ); } . resource-value { font-size : var ( -- text -2 xl ); font-weight : var ( -- weight - medium ); margin-top : var ( -- space -3 ); font-variant-numeric : tabular-nums ; display : flex ; align-items : baseline ; flex-wrap : wrap ; gap : 7 px ; } . resource-value small { font-size : var ( -- text - sm ); color : var ( -- text - tertiary ); font-weight : var ( -- weight - regular ); } . resources p { color : var ( -- text - tertiary ); font-size : 11 px ; line-height : 1.5 ; margin : var ( -- space -2 ) 0 0 ; }
. freshness { display : flex ; justify-content : space-between ; flex-wrap : wrap ; gap : var ( -- space -2 ); color : var ( -- text - tertiary ); font-size : 11 px ; margin-top : -6 px ; }
. warnings , . refresh-error { border-left : 2 px solid var ( -- state - warning ); padding : var ( -- space -3 ) var ( -- space -4 ); background : var ( -- surface ); border-radius : 0 var ( -- radius - sm ) var ( -- radius - sm ) 0 ; font-size : var ( -- text - xs ); color : var ( -- text - secondary ); line-height : 1.6 ; } . warnings p { margin : 0 ; } . warnings p + p { margin-top : var ( -- space -2 ); } . refresh-error { display : grid ; gap : var ( -- space -1 ) ; }
. hosts { display : grid ; gap : var ( -- space -4 ); } . section-heading { display : flex ; justify-content : space-between ; align-items : center ; gap : var ( -- space -4 ); } h2 { font-size : var ( -- text - lg ); margin : 0 ; } . section-heading p { font-size : var ( -- text - sm ); color : var ( -- text - tertiary ); margin : 6 px 0 0 ; } . section-heading > span { color : var ( -- text - secondary ); font-size : var ( -- text - xs ); white-space : nowrap ; }
. host-grid { display : grid ; grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : var ( -- space -4 ); } . host-card , . empty-hosts { background : var ( -- surface ); border : 1 px solid var ( -- border - subtle ); border-radius : var ( -- radius - lg ); padding : var ( -- space -5 ); min-width : 0 ; } . host-title { display : flex ; justify-content : space-between ; gap : var ( -- space -3 ); align-items : center ; } h3 { font-size : var ( -- text - md ); margin : 0 ; } . host-title h3 { overflow-wrap : anywhere ; } . host-title a { text-decoration : none ; color : var ( -- text - primary ); } . host-title a : hover { color : var ( -- accent - primary - hover ); } . host-title a span { color : var ( -- text - tertiary ); font-size : var ( -- text - sm ); }
. status { padding : 3 px 8 px ; font-size : 11 px ; background : var ( -- surface - raised ); color : var ( -- text - secondary ); border : 1 px solid var ( -- border - subtle ); border-radius : var ( -- radius - pill ); white-space : nowrap ; } . status . eligible { color : var ( -- state - online ) ; }
. host-estimate { display : flex ; align-items : center ; gap : var ( -- space -3 ); margin-top : var ( -- space -5 ); } . host-estimate strong { font-size : var ( -- text -2 xl ); font-weight : var ( -- weight - medium ); font-variant-numeric : tabular-nums ; } . host-estimate span { font-size : var ( -- text - xs ); color : var ( -- text - secondary ); } . host-estimate small { display : block ; color : var ( -- text - tertiary ); margin-top : 4 px ; font-size : 11 px ; }
. game-tags { display : flex ; flex-wrap : wrap ; gap : var ( -- space -2 ); margin-top : var ( -- space -3 ); } . game-tags span { font-size : 10 px ; color : var ( -- text - secondary ); background : var ( -- surface - sunken ); padding : 4 px 7 px ; border-radius : 4 px ; }
. host-resources { display : grid ; grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : var ( -- space -3 ); margin : var ( -- space -5 ) 0 0 ; padding : var ( -- space -4 ) 0 ; border-top : 1 px solid var ( -- border - subtle ); border-bottom : 1 px solid var ( -- border - subtle ); } . host-resources dt { font-size : 11 px ; color : var ( -- text - tertiary ); } . host-resources dd { margin : 8 px 0 5 px ; font-size : var ( -- text - md ); font-variant-numeric : tabular-nums ; } . host-resources dd small , . host-resources div > span { font-size : 11 px ; color : var ( -- text - tertiary ) ; }
. host-reasons { padding-left : 16 px ; font-size : var ( -- text - xs ); color : var ( -- text - secondary ); line-height : 1.7 ; margin : var ( -- space -3 ) 0 0 ; } . allowances { margin-top : var ( -- space -4 ); } summary { cursor : pointer ; font-size : var ( -- text - xs ); color : var ( -- text - secondary ); padding : 4 px 0 ; } . allowance-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : var ( -- space -3 ); background : var ( -- surface - sunken ); border-radius : var ( -- radius - sm ); padding : var ( -- space -3 ); margin-top : var ( -- space -3 ); } . allowance-grid strong { font-size : var ( -- text - xs ); font-weight : var ( -- weight - medium ); } . allowance-grid p { font-size : var ( -- text - xs ); margin : 8 px 0 4 px ; color : var ( -- text - secondary ); } . allowance-grid span { font-size : 10 px ; color : var ( -- text - tertiary ); } . allowances > p , . host-updated { font-size : 11 px ; line-height : 1.6 ; color : var ( -- text - tertiary ); margin : var ( -- space -3 ) 0 0 ; }
. methodology { padding : 0 2 px ; } . methodology p { font-size : var ( -- text - xs ); color : var ( -- text - tertiary ); line-height : 1.7 ; max-width : 115 ch ; } . empty-hosts p { color : var ( -- text - secondary ); font-size : var ( -- text - sm ); line-height : 1.6 ; }
@ media ( max-width : 900px ) { . resources { grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr )); } . host-grid { grid-template-columns : 1 fr ; } }
@ media ( max-width : 600px ) { . page { gap : var ( -- space -4 ); } . filters { grid-template-columns : 1 fr 1 fr ; width : 100 % ; } . overview { padding : var ( -- space -5 ); flex-direction : column ; align-items : stretch ; gap : var ( -- space -4 ); } . estimate-number { font-size : 48 px ; } . queue-context { flex-basis : auto ; border-left : 0 ; border-top : 1 px solid var ( -- border - subtle ); padding : var ( -- space -3 ) 0 0 ; } . queue-context p { margin-top : 4 px ; } . queue-context strong { font-size : var ( -- text - xl ); } . resources article { padding : var ( -- space -4 ); } . resource-value { font-size : 28 px ; } . host-card { padding : var ( -- space -4 ); } . section-heading { align-items : flex-start ; } . ranges { width : 100 % ; } . ranges a { text-align : center ; flex : 1 ; } }
. page { display :grid ; gap :var ( -- space - 5 ); min - width :0 ; }
. toolbar ,. overview - heading ,. section - heading { display :flex ; align - items :center ; justify - content :space - between ; flex - wrap :wrap ; gap :16px ; }
. toolbar { align - items :end ; }
. filters { display :grid ; grid - template - columns : minmax ( 180 px , 260 px ) minmax ( 180 px , 260 px ); gap :12px ; }
. freshness { display :flex ; align - items :center ; gap :8px ; color :var ( -- text - secondary ); font - size :var ( -- text - xs ) ; }
. overview { padding :20px 24 px ; border :1px solid var ( -- border - subtle ); border - radius :var ( -- radius - md ); background :var ( -- surface ); }
h2 { margin :0 ; font - size :var ( -- text - lg ); font - weight :500 ; } . overview h2 { font - size :var ( -- text - xl ); } . overview strong { font - weight :600 ; }
. overview p { margin :8px 0 0 ; color :var ( -- text - secondary ); font - size :var ( -- text - xs ) ; }
. queue - context { display :grid ; gap :4px ; font - size :var ( -- text - sm ); color :var ( -- text - secondary ); }
. queue - context a { color :var ( -- accent - primary - hover ); text - decoration :none ; } . queue - context a :hover { text - decoration :underline ; } . queue - context small { font - size :var ( -- text - xs ); }
. resources { display :flex ; flex - wrap :wrap ; gap :16px 40 px ; padding - top :16px ; margin :16px 0 0 ; border - top :1px solid var ( -- border - subtle ) ; }
dt { color :var ( -- text - secondary ); font - size :var ( -- text - xs ); } dd { margin :5px 0 0 ; font - size :var ( -- text - sm ); font - variant - numeric :tabular - nums ; }
. warnings ,. refresh - error { border - left :2px solid var ( -- state - warning ); padding :12px 16 px ; background :var ( -- surface ); font - size :var ( -- text - xs ); color :var ( -- text - secondary ); line - height :1.6 ; }
. warnings p { margin :0 ; } . warnings p + p { margin - top :8px ; } . refresh - error { display :grid ; gap :4px ; }
. hosts { display :grid ; gap :14px ; } . section - heading > span { color :var ( -- text - secondary ); font - size :var ( -- text - xs ); }
. empty - hosts { background :var ( -- surface ); border :1px solid var ( -- border - subtle ); border - radius :var ( -- radius - md ); padding :20px ; }
. empty - hosts h3 { margin :0 ; font - size :var ( -- text - md ); } . empty - hosts p { color :var ( -- text - secondary ); font - size :var ( -- text - sm ) ; }
summary { cursor :pointer ; color :var ( -- text - secondary ); font - size :var ( -- text - xs ); padding :4px 0 ; }
. methodology p { color :var ( -- text - secondary ); font - size :var ( -- text - xs ); line - height :1.7 ; max - width :110ch ; }
. ranges { display :flex ; gap :3px ; padding :3px ; border :1px solid var ( -- border - subtle ); border - radius :var ( -- radius - md ) ; }
. ranges a { padding :7px 12 px ; border - radius :var ( -- radius - sm ); color :var ( -- text - secondary ); text - decoration :none ; font - size :var ( -- text - xs ); white - space :nowrap ; }
. ranges a [ aria - current ] { color :var ( -- text - primary ); background :var ( -- surface - raised ); }
a :focus - visible , summary :focus - visible { outline :2px solid var ( -- accent - primary ); outline - offset :3px ; }
@media ( max - width :600px ) { . filters { width :100 % ; grid - template - columns :1fr 1 fr ; } . overview { padding :16px ; } . resources { gap :16px 24 px ; } }
</ style >