383 lines
22 KiB
TypeScript
383 lines
22 KiB
TypeScript
/**
|
|
* The side panels a game is read from — cards, yards, blockers, facilities.
|
|
*
|
|
* SHARED between the playable page and the site's replay viewer, for the same reason the board
|
|
* renderers are: a replay is the game being WATCHED rather than played, so it should look like the
|
|
* game. The viewer had three panels against the play page's eight — no facilities, no blockers, no
|
|
* cards, no yards — which meant a replay could not answer "why is nothing moving?", the question a
|
|
* replay mostly exists to answer.
|
|
*
|
|
* Each function returns HTML rather than writing to the DOM, so the two pages can keep their own
|
|
* element ids and their own layout while drawing the same contents.
|
|
*/
|
|
|
|
import type { Frame } from '../sim/view.ts';
|
|
|
|
const esc = (s: string): string =>
|
|
String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);
|
|
|
|
/**
|
|
* A card, drawn as a card.
|
|
*
|
|
* `playable === false` crosshatches it: "not yet playable" had to be read, where a hatch is seen.
|
|
* The tooltip still explains WHY, which a hatch cannot. A replay passes null throughout — nothing
|
|
* there is playable, because nothing there is being played.
|
|
*/
|
|
export function cardRow(name: string, why: string, playable: boolean | null): string {
|
|
return (
|
|
`<div class="handcard${playable === false ? ' unplayable' : ''}"${why ? ` data-tip="${esc(why)}"` : ''} tabindex="0">` +
|
|
`<b>${esc(name)}</b></div>`
|
|
);
|
|
}
|
|
|
|
export function handHtml(f: Frame, canPlay: (boolean | null)[] = []): string {
|
|
return f.hand.length
|
|
? f.hand.map((h, i) => cardRow(h, f.handWhat[i] ?? '', canPlay[i] ?? null)).join('')
|
|
: '<span class="dim">empty</span>';
|
|
}
|
|
|
|
/**
|
|
* The three Department piles and the Salvage Yard, each with its top card and its depth.
|
|
*
|
|
* Only the top card may ever be drawn, so the depth is a count and not a hint: everything below it
|
|
* is out of reach, and choosing where to discard is choosing what to put there.
|
|
*/
|
|
export function pilesHtml(f: Frame): string {
|
|
const pile = (label: string, top: string, depth: number, why: string, extra = '', slot = -1): string => {
|
|
const tip = [why, extra].filter(Boolean).join(' · ');
|
|
// A Department is a DROP TARGET for a discard. The attribute is always emitted; only the play
|
|
// page binds a click to it, and only while a card is waiting to be discarded — so the replay
|
|
// viewer draws exactly the same markup and nothing there is clickable.
|
|
const target = slot >= 0 ? ` data-dept="${slot}"` : '';
|
|
return (
|
|
`<div class="handcard"${target}${tip ? ` data-tip="${esc(tip)}"` : ''} tabindex="0">` +
|
|
`<div class="pilehd"><span>${esc(label)}</span><span class="depth">${depth}</span></div>` +
|
|
`<b>${esc(top)}</b></div>`
|
|
);
|
|
};
|
|
return (
|
|
f.departments
|
|
.map((d, i) => {
|
|
const depth = f.departmentDepth[i] ?? 0;
|
|
const under = depth - 1;
|
|
return pile(
|
|
`Dept ${i + 1}`,
|
|
d,
|
|
depth,
|
|
f.departmentsWhat[i] ?? '',
|
|
under > 0 ? `${under} card${under === 1 ? '' : 's'} buried beneath it and out of reach` : '',
|
|
i,
|
|
);
|
|
})
|
|
.join('') +
|
|
pile(
|
|
'Salvage',
|
|
f.salvage.top,
|
|
f.salvage.depth,
|
|
'',
|
|
'Played cards that did not stay on the board. Swept back into the Home Office deck, with the Departments, when the deck runs out.',
|
|
)
|
|
);
|
|
}
|
|
|
|
/** One yard, by car type. Loaded and empty are separate numbers because they do different jobs. */
|
|
export function yardHtml(rows: { type: string; loaded: number; empty: number }[]): string {
|
|
return rows.length === 0
|
|
? '<span class="dim">empty</span>'
|
|
: rows
|
|
.map(
|
|
(r) =>
|
|
`<span class="stock" data-tip="${esc(r.type)} — ${r.loaded} loaded, ${r.empty} empty">` +
|
|
`<b>${esc(r.type)}</b> ` +
|
|
// Each count is its own target: making up a train is choosing a car OF A TYPE AND A LOAD
|
|
// STATE, which is exactly what these two numbers already distinguish.
|
|
`<span class="ld" data-car="${esc(r.type)}" data-loaded="1">${r.loaded}</span>/` +
|
|
`<span class="mt" data-car="${esc(r.type)}" data-loaded="0">${r.empty}</span></span>`,
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
/**
|
|
* THE TIMETABLE: twelve Stages of a Day, and which train is due out at each.
|
|
*
|
|
* Playing a train card rolls 1D12 for a slot, and the roll landed nowhere a player could see — the
|
|
* card was gone and the answer to "so when does it run?" was one line of history. The Frame has
|
|
* carried the twelve slots all along and only the standalone replay ever drew them.
|
|
*
|
|
* `justSet` is the slot filled by the roll being reported this frame, so it can be flashed. It marks
|
|
* the moment rather than the state: the highlight is gone by the next render, which is what makes it
|
|
* read as "that just happened" rather than "this is special".
|
|
*/
|
|
export function timetableHtml(f: Frame, justSet: number | null): string {
|
|
const slots = f.timetable
|
|
.map((t, k) => {
|
|
const stage = k + 1;
|
|
const cls = [
|
|
t !== null ? 'due' : '',
|
|
stage === f.stage ? 'now' : '',
|
|
stage < f.stage ? 'past' : '',
|
|
k === justSet ? 'fresh' : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
const tip =
|
|
t !== null
|
|
? `Train ${t} departs at Stage ${stage}. A train's NUMBER is its seniority and direction — odd runs west, even runs east — and the Stage it leaves is set by the 1D12 roll made when its card was played, so Train 8 at Stage 6 is normal.`
|
|
: `Stage ${stage} — no train due out.`;
|
|
return (
|
|
`<div class="tt-slot ${cls}" data-tip="${esc(tip)}" tabindex="0">` +
|
|
`<span class="tt-s">${stage}</span>` +
|
|
`<span class="tt-t">${t !== null ? `T${t}` : '·'}</span></div>`
|
|
);
|
|
})
|
|
.join('');
|
|
return `<div class="tt">${slots}</div>`;
|
|
}
|
|
|
|
export function blockedHtml(f: Frame): string {
|
|
return f.blocked.length === 0
|
|
? '<li class="dim">nothing blocked</li>'
|
|
: f.blocked
|
|
.map((b) => `<li class="sev-${b.severity}"><b>${esc(b.where)}</b> — ${esc(b.why)}</li>`)
|
|
.join('');
|
|
}
|
|
|
|
/** The load pipeline at each industry: green → MEN | AT | WORK → red, and the siding. */
|
|
export function facilitiesHtml(f: Frame): string {
|
|
/**
|
|
* One number, with what the card printed and what a Modifier added.
|
|
*
|
|
* `laborers` arrives as "left/total" and the rest as plain totals, so the total is taken from the
|
|
* end either way and compared with the base.
|
|
*/
|
|
const stat = (label: string, shown: string, base: number, why: string): string => {
|
|
const total = Number(shown.split('/').pop() ?? shown);
|
|
const added = Number.isFinite(total) ? total - base : 0;
|
|
const delta = added > 0 ? `<span class="added">+${added}</span>` : '';
|
|
return (
|
|
`<span class="cap" data-tip="${esc(why)}${added > 0 ? ` The card prints ${base}; Modifiers beside it add ${added}.` : ''}">` +
|
|
`<span class="dim">${esc(label)}</span> <b>${esc(shown)}</b>${delta}</span>`
|
|
);
|
|
};
|
|
/**
|
|
* GREEN IS OUTBOUND AND RED IS INBOUND — the caller says which, because this helper cannot know.
|
|
*
|
|
* It used to paint every filled box with one class, so the inbound row rendered GREEN while the
|
|
* board SVG on the same page drew it red. The two disagreed on screen at the same time, which is
|
|
* the one thing a colour code must never do. Reported from playtesting.
|
|
*/
|
|
const boxes = (filled: string[], cap: number, cls: 'g' | 'r' | 's'): string => {
|
|
let h = '';
|
|
for (let i = 0; i < Math.max(cap, filled.length); i++) {
|
|
h += `<span class="box ${i < filled.length ? cls : 'empty'}">${i < filled.length ? esc(filled[i] ?? '') : '·'}</span>`;
|
|
}
|
|
return h || '<span class="dim">—</span>';
|
|
};
|
|
return f.facilities.length === 0
|
|
? '<div class="dim">no facilities yet</div>'
|
|
: f.facilities
|
|
.map(
|
|
(x) =>
|
|
`<div class="fac" data-tip="${esc(x.name)} — ${esc(x.flow)} ${esc(x.commodity)}" tabindex="0">` +
|
|
`<div class="nm">${esc(x.name)} <span class="dim">${esc(x.commodity)}</span></div>` +
|
|
/**
|
|
* THE WORKERS AND THE CAPACITIES, ON THE PANEL RATHER THAN IN A TOOLTIP.
|
|
*
|
|
* Laborers were only ever in the hover text, so "how many actions can I take here this
|
|
* Stage?" — the question that decides every Cargo phase — had to be hunted for. And a
|
|
* Modifier's whole effect is one of these numbers going up, so showing the number alone
|
|
* made an Ice House look like it had done nothing. Each now says what the card prints
|
|
* and what the Modifiers beside it added.
|
|
*/
|
|
`<div class="caps">` +
|
|
/**
|
|
* PORTERS TOO, NOT JUST LABORERS.
|
|
*
|
|
* `porters` has been on the view-model all along and no renderer ever printed it — so a
|
|
* Waiting Area, Restaurant or Hotel beside the Office, whose whole effect is +1 porter
|
|
* and +1 passenger slot, showed exactly half of what it did. Reported: "it gave me an
|
|
* extra porter that I see no indication of anywhere."
|
|
*
|
|
* A facility has one kind of worker or the other: Laborers work freight, Porters work
|
|
* passengers. Showing the row that is always 0/0 would be noise, so each shows its own.
|
|
*/
|
|
(Number(x.laborers.split('/').pop()) > 0 || x.base.laborers > 0
|
|
? stat('laborers', x.laborers, x.base.laborers, 'Actions this facility can take each Stage — one Laborer moves one load one square. Left of the slash is how many are still free this Stage.')
|
|
: '') +
|
|
(Number(x.porters.split('/').pop()) > 0 || x.base.porters > 0
|
|
? stat('porters', x.porters, x.base.porters, 'Passenger work, one action each per Stage: a Porter boards or detrains passengers, and earns the Revenue in a single action. Left of the slash is how many are still free this Stage.')
|
|
: '') +
|
|
(x.allowsOut ? stat('out', String(x.greenCap), x.base.out, 'Green boxes: loads waiting to be worked onto a car.') : '') +
|
|
(x.allowsIn ? stat('in', String(x.redCap), x.base.in, 'Red boxes: loads cleared off an arriving car.') : '') +
|
|
(x.modifiers.length
|
|
? `<span class="mods" data-tip="Modifier cards standing beside this industry, each raising one of the numbers above.">+ ${esc(x.modifiers.join(', '))}</span>`
|
|
: '') +
|
|
/**
|
|
* A GRANT THIS FACILITY CANNOT USE, SAID OUT LOUD.
|
|
*
|
|
* An industry's printed flow is absolute, so an Ice House beside a Grocer's Warehouse
|
|
* gives its Laborer and nothing else. Showing only the numbers made that read as a bug —
|
|
* reported from playtesting as "it added the laborer but not the outbound slot". The
|
|
* number genuinely does not move; what was missing was the reason.
|
|
*/
|
|
(x.suppressed.length
|
|
? `<span class="dead" data-tip="${esc(x.suppressed.join(' · '))}">` +
|
|
`${esc(String(x.suppressed.length))} printed bonus${x.suppressed.length === 1 ? '' : 'es'} unused</span>`
|
|
: '') +
|
|
`</div>` +
|
|
/**
|
|
* IN THE ORDER THE LOAD TRAVELS. §9.3 loads Green → MEN → AT → WORK → the spotted car,
|
|
* and unloads the other way: car → WORK → AT → MEN → red. Labelling the rows by colour
|
|
* alone left a player to work out which direction their industry ran; naming the step
|
|
* says it outright, and an industry that only receives has no green row to puzzle over.
|
|
*/
|
|
(x.allowsOut
|
|
? `<div class="boxes"><span class="dim" data-tip="${
|
|
x.kind === 'passenger'
|
|
? 'Passengers waiting to board. A Porter puts them onto a coach in a single action.'
|
|
: 'Loads waiting to be worked out onto a car. They travel green → MEN → AT → WORK, then onto a spotted empty car.'
|
|
}">${x.kind === 'passenger' ? 'waiting to board' : 'waiting to load'}</span>${boxes(x.green, x.greenCap, 'g')}</div>`
|
|
: '') +
|
|
/**
|
|
* A PASSENGER FACILITY HAS NO SIGN AND NO SIDING, so neither row is drawn for one.
|
|
*
|
|
* `maw` comes back empty for a Depot, Station or Terminal — the pipeline is a Freight
|
|
* Facility fitting (§9.2 is Porters, with no MEN | AT | WORK step). The row LABEL printed
|
|
* ahead of the loop regardless, so a Depot showed the caption and three empty boxes it
|
|
* has no Laborer to work. Same for the siding: an Office has no industry track at all.
|
|
*/
|
|
(x.maw.length > 0
|
|
? `<div class="boxes"><span class="dim" data-tip="One physical sign, worked in whichever direction this industry runs. Only one load may sit on each of the three boxes.">MEN AT WORK</span>` +
|
|
x.maw.map((m) => `<span class="box ${m ? 'm' : 'empty'}">${m ? esc(m) : '·'}</span>`).join('') +
|
|
`</div>`
|
|
: '') +
|
|
(x.allowsIn
|
|
? `<div class="boxes"><span class="dim" data-tip="${
|
|
x.kind === 'passenger'
|
|
? 'Passengers who have arrived and been detrained by a Porter.'
|
|
: 'Loads that have come off an arriving car and been cleared. They travel car → WORK → AT → MEN, then into a red box — the opposite direction to loading.'
|
|
}">${x.kind === 'passenger' ? 'arrived' : 'cleared inbound'}</span>${boxes(x.red, x.redCap, 'r')}</div>`
|
|
: '') +
|
|
(x.trackCap > 0 || x.track.length > 0
|
|
? `<div class="boxes"><span class="dim" data-tip="Cars spotted on this industry's track. A load leaves the sign onto one of these, or an arriving load starts from one.">siding</span>${boxes(x.track, x.trackCap, 's')}</div>`
|
|
: '') +
|
|
`<div class="fstat ${x.jammed ? 'bad' : x.canFinish ? 'good' : 'idle'}" data-tip="${
|
|
x.jammed
|
|
? 'A load is sitting on MEN|AT|WORK with no spotted car to receive it. That locks the industry track, which blocks the very car that would clear it (§9.3).'
|
|
: x.canFinish
|
|
? 'A matching empty car is spotted on the siding, so a load worked here can come off onto it.'
|
|
: 'No matching car is spotted. Starting a load here would park it on WORK and jam the facility.'
|
|
}">` +
|
|
(x.jammed ? 'JAMMED' : x.canFinish ? 'ready' : 'no car spotted') +
|
|
`</div></div>`,
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
/** Styling for all of the above, so the two pages cannot drift apart visually. */
|
|
export const PANEL_CSS = `
|
|
h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#8b94a3;margin:9px 0 4px}
|
|
.cardrow{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 2px}
|
|
.handcard{background:#242c36;border:1px solid #2c333d;border-radius:5px;
|
|
padding:5px 8px;font-size:11px;min-width:96px;position:relative}
|
|
.handcard:focus{outline:2px solid #4d6fa8;outline-offset:1px}
|
|
.cardrow.ref .handcard{background:#1c2129;border-style:dashed;border-color:#39424e;color:#b6bec9}
|
|
.handcard.unplayable{color:#7d8794;border-color:#39424e}
|
|
.handcard.unplayable::after{content:"";position:absolute;inset:0;border-radius:5px;pointer-events:none;
|
|
background:repeating-linear-gradient(45deg,transparent 0 5px,rgba(150,160,175,.20) 5px 6px)}
|
|
/* THE CARD JUST DRAWN. It sits first in the row, and this says which one that is — three cards that
|
|
look alike otherwise, with nothing to distinguish the one you turned over. Green is the page's
|
|
"a good thing just happened" colour, as on the timetable. A static badge rather than a flash: the
|
|
hand is rebuilt by innerHTML on every render, which would restart a keyframe each time, and the
|
|
question being answered is "which of these is new" rather than "did something happen". */
|
|
.handcard.fresh{border-color:#8fd6a0;box-shadow:0 0 0 2px rgba(143,214,160,.18)}
|
|
.handcard.fresh::before{content:"NEW";display:block;font-size:9px;letter-spacing:.09em;
|
|
color:#8fd6a0;font-weight:700;margin-bottom:2px}
|
|
/* A pile shows two things: which card is face up on top, and how many are under it. */
|
|
.pilehd{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:2px;
|
|
font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8b94a3}
|
|
.pilehd .depth{font-variant-numeric:tabular-nums;background:#2a3038;border-radius:8px;
|
|
padding:0 6px;color:#cfe0f5}
|
|
/* Rolling stock is finite and the Classification Yard only returns it when the Division Yard is
|
|
bare, so watching the Division Yard run down is real information. */
|
|
.yard{display:flex;flex-wrap:wrap;gap:5px;margin:0 0 6px}
|
|
.stock{background:#1c2129;border:1px solid #39424e;border-radius:4px;padding:2px 7px;font-size:11px}
|
|
.stock b{color:#e6e9ee}
|
|
.stock .ld{color:#8fd6a0}
|
|
.stock .mt{color:#9fb6d8}
|
|
.stock.none{opacity:.4}
|
|
/* A PILE THAT IS WAITING TO BE PICKED, drawn like the board's ghost squares because it is the same
|
|
question: you have chosen a card, now choose where it goes. It was a 2px outline on a small tile
|
|
in a panel the player might not be looking at, and was reported as "a very tiny highlight". Now it
|
|
grows, brightens, says DISCARD HERE outright, and pulses once so the eye finds it. */
|
|
.handcard.target{border:2px dashed #5aa9e6;background:#233246;cursor:pointer;color:#e6e9ee;
|
|
box-shadow:0 0 0 4px rgba(90,169,230,.22);transform:translateY(-2px);
|
|
animation:pick 1.1s ease-out 2}
|
|
.handcard.target:hover{background:#31485f;border-style:solid;box-shadow:0 0 0 6px rgba(90,169,230,.30)}
|
|
.handcard.target::before{content:"DISCARD HERE";display:block;font-size:9px;letter-spacing:.09em;
|
|
color:#8fc4ee;font-weight:700;margin-bottom:2px}
|
|
@keyframes pick{0%{box-shadow:0 0 0 4px rgba(90,169,230,.22)}
|
|
50%{box-shadow:0 0 0 9px rgba(90,169,230,.10)}
|
|
100%{box-shadow:0 0 0 4px rgba(90,169,230,.22)}}
|
|
@media(prefers-reduced-motion:reduce){.handcard.target{animation:none}}
|
|
/* And the piles that are NOT targets step back while a discard is being aimed, so the three that
|
|
are stand out from the Salvage Yard beside them. */
|
|
.cardrow.aiming .handcard:not(.target){opacity:.4}
|
|
.stock .ld.addable,.stock .mt.addable{cursor:pointer;border-radius:3px;padding:0 4px;
|
|
outline:1px solid #5aa9e6;background:rgba(90,169,230,.16)}
|
|
.stock .ld.addable:hover,.stock .mt.addable:hover{background:rgba(90,169,230,.34)}
|
|
/* Twelve Stages across, so a Day is one glance. The current Stage is lit, Stages already gone are
|
|
dimmed, and a slot the die has just filled flashes once. */
|
|
.tt{display:flex;gap:3px;flex-wrap:wrap}
|
|
.tt-slot{flex:1 1 0;min-width:26px;text-align:center;border:1px solid #2c333d;border-radius:4px;
|
|
padding:2px 0;background:#1a1f26;font-size:10px;cursor:help;line-height:1.25}
|
|
.tt-slot .tt-s{display:block;color:#5f6b7a;font-size:9px}
|
|
.tt-slot .tt-t{display:block;color:#5f6b7a;font-variant-numeric:tabular-nums}
|
|
.tt-slot.due{background:#222c38;border-color:#4d6fa8}
|
|
.tt-slot.due .tt-t{color:#cfe0f5;font-weight:700}
|
|
.tt-slot.past{opacity:.45}
|
|
.tt-slot.now{border-color:#b98cf0;box-shadow:0 0 0 2px rgba(150,110,230,.20)}
|
|
.tt-slot.now .tt-s{color:#b98cf0;font-weight:700}
|
|
.tt-slot.fresh{animation:ttflash 1.5s ease-out 1}
|
|
@keyframes ttflash{0%{background:#2f6b47;border-color:#8fd6a0;transform:scale(1.18)}
|
|
100%{background:#222c38;border-color:#4d6fa8;transform:scale(1)}}
|
|
@media(prefers-reduced-motion:reduce){.tt-slot.fresh{animation:none;outline:2px solid #8fd6a0}}
|
|
/* THE KEY TO THE FOUR STATES ABOVE. A slot can be lit blue, ringed violet, dimmed, or flashing
|
|
green, and nothing said which was which — reported as "what do the colours mean?" The green one
|
|
in particular is a MOMENT, not a state: it marks the slot the die just filled and is gone by the
|
|
next render, so without a key it reads as a category of train. */
|
|
.tt-key{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px;font-size:10px;color:#8b94a3}
|
|
.tt-key span{display:inline-flex;align-items:center;gap:4px}
|
|
.tt-key span::before{content:"";width:9px;height:9px;border-radius:2px;border:1px solid #2c333d;
|
|
background:#1a1f26}
|
|
.tt-key .k-due::before{background:#222c38;border-color:#4d6fa8}
|
|
.tt-key .k-now::before{border-color:#b98cf0;box-shadow:0 0 0 1px rgba(150,110,230,.35)}
|
|
.tt-key .k-past::before{opacity:.45}
|
|
.tt-key .k-fresh::before{background:#2f6b47;border-color:#8fd6a0}
|
|
ul.blocked{margin:0;padding-left:18px}
|
|
.sev-waiting{color:#8b94a3}.sev-risk{color:#e0b060}.sev-stuck{color:#e58080}
|
|
.fac{border-top:1px solid #2c333d;padding:7px 0}
|
|
.fac:first-child{border-top:0}
|
|
.fac .nm{font-size:12px;font-weight:600}
|
|
.caps{display:flex;flex-wrap:wrap;gap:9px;margin-top:3px;font-size:10px;align-items:baseline}
|
|
.cap b{color:#e6e9ee;font-variant-numeric:tabular-nums}
|
|
.cap .added{color:#8fd6a0;margin-left:2px;font-weight:700}
|
|
.caps .mods{color:#c8a04a}
|
|
/* Amber-grey: a fact about the card, not a fault the player caused. */
|
|
.caps .dead{color:#8b94a3;border-bottom:1px dotted #6c7480}
|
|
.boxes{display:flex;gap:3px;align-items:center;margin-top:3px;flex-wrap:wrap}
|
|
.box{display:inline-block;min-width:22px;text-align:center;border-radius:3px;padding:1px 4px;font-size:10px}
|
|
.box.empty{background:#242a32;color:#5a6472}
|
|
/* Green OUT, red IN, matching the board SVG's palette exactly — the panel and the map are the same
|
|
card seen twice, so they may not disagree about which colour means which way. Grey for the siding,
|
|
which is a place rather than a direction. */
|
|
.box.g{background:#2f6b3d}
|
|
.box.r{background:#8a4a4a}
|
|
.box.s{background:#3a4450}
|
|
.box.m{background:#8a6d1f}
|
|
.fstat{margin-top:3px;font-size:10px;padding:1px 6px;border-radius:3px;display:inline-block}
|
|
.fstat.good{background:rgba(40,140,60,.28)}
|
|
.fstat.bad{background:rgba(190,50,50,.38);font-weight:700}
|
|
.fstat.idle{opacity:.6}
|
|
`;
|