A4 character sheet (summary and lobby), A4/wide switch, print to PDF
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { applyPreset, applyProfession, compute, effectiveRules, emptyCharacter, kitItems, normalize, resetStep, setProfessionPicks } from './calc.js'
|
||||
import SheetPanel from './SheetPanel.jsx'
|
||||
import Tip from './Tip.jsx'
|
||||
import PaperSheet from './PaperSheet.jsx'
|
||||
import { explainAttr, explainAttrPool, explainCapacity, explainCarry, explainDerived, explainProfPool, explainSkill, explainSkillPool } from './explain.js'
|
||||
|
||||
const STEPS = ['Concept', 'Attributes', 'Profession', 'Skills', 'Talents', 'Mutations', 'Equipment', 'Summary']
|
||||
@@ -433,47 +434,7 @@ export default function Creator({ rules: baseRules, user, loadRequest, clearLoad
|
||||
<>
|
||||
<h2>Summary</h2>
|
||||
{warnings.length > 0 && <ul className="warn">{warnings.map((w) => <li key={w}>{w}</li>)}</ul>}
|
||||
<div className="sheet card">
|
||||
<h3 className="sheet-name">{ch.name || 'Nameless'}</h3>
|
||||
<p className="muted">{ch.player && `Player: ${ch.player} · `}Profession: {prof?.name || '–'}{cls && ` · Class: ${[cls.name, sub?.name].filter(Boolean).join(' / ')}`}</p>
|
||||
<div className="sheet-cols">
|
||||
<div>
|
||||
<h4>Attributes</h4>
|
||||
<table><tbody>{rules.attributes.map((a) => <tr key={a.id}><td><Tip text={explainAttr(a, ctx)}>{a.name}</Tip></td><td>{c.attrs[a.id]}</td><td className="muted">Mod {c.mods[a.id]}</td></tr>)}</tbody></table>
|
||||
<h4>Derived stats</h4>
|
||||
<table><tbody>{rules.derived.map((d) => <tr key={d.id}><td><Tip text={explainDerived(d, ctx)}>{d.name}</Tip></td><td>{c.derived[d.id]}</td></tr>)}</tbody></table>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Talents</h4>
|
||||
<ul className="desclist">{rules.talents.filter((t) => ch.talents.includes(t.id)).map((t) => <li key={t.id}><strong>{t.name}</strong><div className="rowdesc">{t.desc}</div></li>)}</ul>
|
||||
{!ch.talents.length && <p>–</p>}
|
||||
<h4><Tip text={explainCapacity(ctx)}>Mutations / implants ({c.capacityUsed}/{c.capacityMax})</Tip></h4>
|
||||
<ul className="desclist">{rules.augments.filter((a) => ch.augments.includes(a.id)).map((a) => <li key={a.id}><strong>{a.name}</strong> <span className="muted small">cost {a.cost}</span><div className="rowdesc">{a.desc}</div></li>)}</ul>
|
||||
{!ch.augments.length && <p>–</p>}
|
||||
<h4><Tip text={explainCarry(ctx)}>Equipment ({c.weight}/{c.carryMax} kg)</Tip></h4>
|
||||
<ul className="desclist">{gearLines.map((g) => <li key={g.item.id}><strong>{g.item.name}{g.qty > 1 ? ` x${g.qty}` : ''}</strong><div className="rowdesc">{[g.item.stat, `${Math.round(g.item.kg * g.qty * 10) / 10} kg`, g.item.note].filter(Boolean).join(' · ')}</div></li>)}</ul>
|
||||
{!gearLines.length && <p>–</p>}
|
||||
{ch.gear.other && <p className="small"><strong>Other:</strong> {ch.gear.other}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<h4>Skills</h4>
|
||||
<div className="skillcols">
|
||||
{rules.skill_groups.map((g) => (
|
||||
<div key={g.id} className="skillgroup">
|
||||
<h5>{g.name}</h5>
|
||||
<table><tbody>
|
||||
{c.skills.filter((s) => s.group === g.id).map((s) => (
|
||||
<tr key={s.id} className={s.usable ? '' : 'dim'}>
|
||||
<td><Tip text={explainSkill(s, ctx)}>{s.name}</Tip>{s.type === 'advanced' && <span className="muted small"> (adv.)</span>}</td>
|
||||
<td>{s.total}</td>
|
||||
<td className="muted">{s.pips === 2 ? 'Expertise' : s.pips === 1 ? 'Proficient' : !s.usable ? 'Locked' : s.points ? `+${s.points}` : ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody></table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<PaperSheet data={ch} baseRules={baseRules} />
|
||||
<div className="actions wrap">
|
||||
<button onClick={() => download('pdf')}>Download as PDF</button>
|
||||
<button onClick={() => download('docx')}>Download as Word</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import SheetView from './SheetView.jsx'
|
||||
import PaperSheet from './PaperSheet.jsx'
|
||||
|
||||
const J = { 'Content-Type': 'application/json' }
|
||||
const send = (url, method, body) => fetch(url, { method, headers: J, body: body === undefined ? undefined : JSON.stringify(body) })
|
||||
@@ -320,6 +321,14 @@ function CharTab({ cid, owner, rules, onOpen, name }) {
|
||||
const t = setInterval(() => !document.hidden && load(), 15000)
|
||||
return () => clearInterval(t)
|
||||
}, [cid])
|
||||
const [view, setView] = useState(() => { try { return localStorage.getItem('aw_sheetview') === 'wide' ? 'wide' : 'paper' } catch { return 'paper' } })
|
||||
const pick = (v) => { setView(v); try { localStorage.setItem('aw_sheetview', v) } catch { /* storage may be blocked */ } }
|
||||
const viewSwitch = (
|
||||
<span className="seg" role="group" aria-label="Sheet layout">
|
||||
<button className={view === 'paper' ? 'on' : 'ghost'} onClick={() => pick('paper')} title="A4 page layout, compact">A4 sheet</button>
|
||||
<button className={view === 'wide' ? 'on' : 'ghost'} onClick={() => pick('wide')} title="Expand to the full width">Wide</button>
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
@@ -328,7 +337,8 @@ function CharTab({ cid, owner, rules, onOpen, name }) {
|
||||
</div>
|
||||
<p className="muted small">{owner ? `Played by ${owner}. ` : ''}Read-only, hover or tap a value to see how it is made up.</p>
|
||||
{err && <p className="warn">{err}</p>}
|
||||
{data && <SheetView data={data} baseRules={rules} />}
|
||||
{data && view === 'paper' && <PaperSheet data={data} baseRules={rules} tools={viewSwitch} />}
|
||||
{data && view === 'wide' && <><div className="papertools">{viewSwitch}</div><SheetView data={data} baseRules={rules} /></>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import Tip from './Tip.jsx'
|
||||
import { compute, effectiveRules, kitItems, normalize } from './calc.js'
|
||||
import { explainAttr, explainCapacity, explainCarry, explainDerived, explainSkill } from './explain.js'
|
||||
|
||||
const PAPER_PX = 794 // 210 mm at 96 dpi
|
||||
|
||||
// Opens the sheet pages in a clean window (only the paper, A4, no margins) and starts the print dialog:
|
||||
// "Save as PDF" there gives an A4 PDF that looks exactly like the screen.
|
||||
export function printSheet(root, title) {
|
||||
const win = window.open('', '_blank')
|
||||
if (!win) { window.alert('Please allow pop-ups for this site to print the sheet.'); return }
|
||||
const clone = root.cloneNode(true)
|
||||
clone.style.zoom = ''
|
||||
const head = [...document.querySelectorAll('link[rel=stylesheet], style')].map((n) => n.outerHTML).join('')
|
||||
win.document.open()
|
||||
win.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${(title || 'Character').replace(/[<>&]/g, '')}</title>${head}
|
||||
<style>@page { size: A4; margin: 0 } html, body { background: #fff !important; margin: 0; padding: 0 }
|
||||
.papers { display: block !important; gap: 0 !important } .paper { border: 0 !important; box-shadow: none !important; margin: 0 !important; min-height: 296.5mm !important; break-after: page; }
|
||||
.paper:last-child { break-after: auto }</style></head><body>${clone.outerHTML}</body></html>`)
|
||||
win.document.close()
|
||||
const go = () => { win.focus(); win.print() }
|
||||
const links = [...win.document.querySelectorAll('link[rel=stylesheet]')]
|
||||
let pending = links.length
|
||||
if (!pending) return go()
|
||||
links.forEach((l) => { l.addEventListener('load', () => { if (--pending === 0) setTimeout(go, 150) }); l.addEventListener('error', () => { if (--pending === 0) go() }) })
|
||||
}
|
||||
|
||||
// Scales the A4 pages down when the space is narrower than the paper
|
||||
function Fit({ children, rootRef }) {
|
||||
const wrap = useRef(null)
|
||||
const [z, setZ] = useState(1)
|
||||
useLayoutEffect(() => {
|
||||
const f = () => wrap.current && setZ(Math.min(1, wrap.current.clientWidth / PAPER_PX))
|
||||
f()
|
||||
const ro = new ResizeObserver(f)
|
||||
ro.observe(wrap.current)
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
return <div ref={wrap} className="paperfit"><div ref={rootRef} className="papers" style={{ zoom: z }}>{children}</div></div>
|
||||
}
|
||||
|
||||
const Pips = ({ n, locked }) => (
|
||||
<span className={`ppips ${locked ? 'locked' : ''}`} aria-label={n === 2 ? 'Expertise' : n === 1 ? 'Proficient' : locked ? 'Locked' : ''}>
|
||||
<i className={n >= 1 ? 'on' : ''} /><i className={n >= 2 ? 'on' : ''} />
|
||||
</span>
|
||||
)
|
||||
|
||||
// The character sheet as A4 pages (page 1: attributes and skills, page 2: talents, mutations, equipment, notes).
|
||||
export default function PaperSheet({ data, baseRules, tools }) {
|
||||
const ch = useMemo(() => normalize(data, baseRules), [data, baseRules])
|
||||
const rules = useMemo(() => effectiveRules(baseRules, ch), [baseRules, ch])
|
||||
const c = useMemo(() => compute(ch, rules), [ch, rules])
|
||||
const x = { ch, rules, c }
|
||||
const root = useRef(null)
|
||||
|
||||
const prof = rules.professions.find((p) => p.id === ch.profession)
|
||||
const cls = rules.classes.find((k) => k.id === ch.class_id)
|
||||
const sub = cls?.subclasses.find((s) => s.id === ch.subclass_id)
|
||||
const preset = ch.preset === 'custom' ? 'Custom' : baseRules.presets?.find((p) => p.id === ch.preset)?.name || 'Custom'
|
||||
const short = (s) => rules.attributes.filter((a) => s.attrs.includes(a.id)).map((a) => a.short).join('/')
|
||||
const talents = rules.talents.filter((t) => ch.talents.includes(t.id))
|
||||
const augments = rules.augments.filter((a) => ch.augments.includes(a.id))
|
||||
const gear = kitItems(ch, rules)
|
||||
const name = ch.name || 'Nameless'
|
||||
const sign = (n) => (n > 0 ? `+${n}` : `${n}`)
|
||||
|
||||
const Field = ({ label, children }) => <div className="pfield"><span>{label}</span><b>{children || '–'}</b></div>
|
||||
const Foot = ({ n }) => <div className="pfoot"><span>Arcane Wasteland</span><span>{name}</span><span>Page {n} / 2</span></div>
|
||||
|
||||
return (
|
||||
<div className="papersheet">
|
||||
<div className="papertools">
|
||||
{tools}
|
||||
<button className="ghost" onClick={() => printSheet(root.current, name)}>Print / save as PDF (A4)</button>
|
||||
</div>
|
||||
<Fit rootRef={root}>
|
||||
<section className="paper">
|
||||
<header className="phead">
|
||||
<div className="pname">{name}</div>
|
||||
<div className="pbrand">Arcane Wasteland<span>Character sheet</span></div>
|
||||
</header>
|
||||
<div className="pfields">
|
||||
<Field label="Player">{ch.player}</Field>
|
||||
<Field label="Concept">{ch.concept}</Field>
|
||||
<Field label="Class">{cls && [cls.name, sub?.name].filter(Boolean).join(' / ')}</Field>
|
||||
<Field label="Profession">{prof?.name}</Field>
|
||||
<Field label="Rules">{preset}</Field>
|
||||
</div>
|
||||
|
||||
<div className="pattrs">
|
||||
{rules.attributes.map((a) => (
|
||||
<div key={a.id} className="pattr">
|
||||
<Tip text={explainAttr(a, x)}><span className="pattr-n">{a.name}</span></Tip>
|
||||
<b className="pattr-v">{c.attrs[a.id]}</b>
|
||||
<span className="pattr-m">Mod {sign(c.mods[a.id])}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="pderived">
|
||||
{rules.derived.map((d) => (
|
||||
<div key={d.id} className="pstat">
|
||||
<Tip text={explainDerived(d, x)}><span>{d.name}</span></Tip>
|
||||
<b>{c.derived[d.id]}</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="psec">
|
||||
<h3>Skills <small>● proficient · ●● expertise · attribute · points · total</small></h3>
|
||||
<div className="pskills">
|
||||
{rules.skill_groups.map((g) => (
|
||||
<div key={g.id} className="pgroup">
|
||||
<h4>{g.name}</h4>
|
||||
{c.skills.filter((s) => s.group === g.id).map((s) => (
|
||||
<div key={s.id} className={`prow ${s.usable ? '' : 'locked'}`}>
|
||||
<Pips n={s.pips} locked={!s.usable} />
|
||||
<Tip text={explainSkill(s, x)}><span className="pn">{s.name}{s.type === 'advanced' && <em> adv</em>}</span></Tip>
|
||||
<span className="pa">{short(s)}</span>
|
||||
<span className="pp">{s.points || ''}</span>
|
||||
<b className="pt">{s.total}</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="psec pnotes"><h3>Conditions & notes</h3><div className="plines" /></div>
|
||||
<Foot n={1} />
|
||||
</section>
|
||||
|
||||
<section className="paper">
|
||||
<header className="phead small">
|
||||
<div className="pname">{name}</div>
|
||||
<div className="pbrand">{[cls?.name, prof?.name].filter(Boolean).join(' · ')}</div>
|
||||
</header>
|
||||
|
||||
<div className="psec">
|
||||
<h3>Talents</h3>
|
||||
{talents.length === 0 && <p className="pnone">–</p>}
|
||||
<div className="pcols">
|
||||
{talents.map((t) => <div key={t.id} className="pitem"><b>{t.name}</b><p>{t.desc}</p></div>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="psec">
|
||||
<h3><Tip text={explainCapacity(x)}>Mutations & implants</Tip> <small>Capacity {c.capacityUsed} / {c.capacityMax}</small></h3>
|
||||
{augments.length === 0 && <p className="pnone">–</p>}
|
||||
<div className="pcols">
|
||||
{augments.map((a) => (
|
||||
<div key={a.id} className="pitem"><b>{a.name}</b> <span className="ptag">{a.kind === 'implant' ? 'Implant' : 'Mutation'} · cost {a.cost}</span><p>{a.desc}</p></div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="psec">
|
||||
<h3><Tip text={explainCarry(x)}>Equipment</Tip> <small>Carried {c.weight} / {c.carryMax} kg</small></h3>
|
||||
{gear.length === 0 && <p className="pnone">–</p>}
|
||||
{gear.length > 0 && (
|
||||
<table className="pgear"><thead><tr><th>Item</th><th>Type</th><th>Stats</th><th>Qty</th><th>kg</th></tr></thead>
|
||||
<tbody>{gear.map((g) => (
|
||||
<tr key={g.item.id}><td><b>{g.item.name}</b>{g.item.note && <span className="pnote"> {g.item.note}</span>}</td><td>{g.label}</td><td>{g.item.stat}</td>
|
||||
<td>{g.qty}</td><td>{Math.round(g.item.kg * g.qty * 10) / 10}</td></tr>
|
||||
))}</tbody></table>
|
||||
)}
|
||||
{ch.gear?.other && <p className="pother"><b>Other:</b> {ch.gear.other}</p>}
|
||||
</div>
|
||||
|
||||
{ch.background && <div className="psec"><h3>Background</h3><p className="pbg">{ch.background}</p></div>}
|
||||
|
||||
<div className="psec pnotes"><h3>Notes</h3><div className="plines" /></div>
|
||||
<Foot n={2} />
|
||||
</section>
|
||||
</Fit>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+67
-1
@@ -208,7 +208,7 @@ h2 + .charlist, .card + h2 { margin-top: 1.4rem; }
|
||||
/* lobby desktop layout: main area left, chat column fixed at the right edge of the window, dice bar bottom centre of the main area */
|
||||
:root { --chatw: 380px; --hdr: 3.7rem; }
|
||||
body:has(.lobbychat) main { max-width: none; margin: 0; padding-right: calc(var(--chatw) + 1.5rem); }
|
||||
.lobbypage { max-width: 1200px; margin: 0 auto; padding-bottom: 8rem; }
|
||||
.lobbypage { max-width: 1200px; margin: 0; padding-bottom: 8rem; }
|
||||
.lobbychat { position: fixed; right: 0; top: var(--hdr); bottom: 0; width: var(--chatw); z-index: 10; border-radius: 0; border-width: 0 0 0 1px; padding: .8rem; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.lobbychat .chatwrap { flex: 1; min-height: 0; grid-template-rows: minmax(0, 1fr) auto auto auto auto; }
|
||||
.lobbychat .chatbox { height: auto; min-height: 0; }
|
||||
@@ -251,3 +251,69 @@ body:has(.lobbychat) main { max-width: none; margin: 0; padding-right: calc(var(
|
||||
.lobbycard { align-content: start; }
|
||||
.lobbycard .chips { align-items: flex-start; align-content: flex-start; }
|
||||
.lobbycard .chip { align-self: flex-start; }
|
||||
|
||||
/* ---- A4 character sheet (fixed colours on purpose: it is printed on white paper) ---- */
|
||||
.papertools { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: .4rem 0 .8rem; }
|
||||
.seg { display: inline-flex; }
|
||||
.seg button { border-radius: 0; padding: .4rem .9rem; }
|
||||
.seg button:first-child { border-radius: var(--radius) 0 0 var(--radius); }
|
||||
.seg button:last-child { border-radius: 0 var(--radius) var(--radius) 0; margin-left: -1px; }
|
||||
.seg button.on { background: var(--noxium); color: #12150f; border-color: var(--noxium); }
|
||||
.paperfit { width: 100%; }
|
||||
.papers { display: grid; gap: 14px; justify-items: start; width: 210mm; }
|
||||
.paper { box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 11mm 12mm 8mm; background: #fff; color: #1a1f14; border: 1px solid #cfd5c3; box-shadow: 0 2px 12px rgba(0, 0, 0, .14);
|
||||
display: flex; flex-direction: column; gap: 3.5mm; font-size: 8.6pt; line-height: 1.25; break-after: page; }
|
||||
.paper * { color: inherit; }
|
||||
.paper .tipped { border-bottom: 0; text-decoration: none; cursor: default; }
|
||||
.paper p { margin: 0; }
|
||||
.phead { display: flex; justify-content: space-between; align-items: flex-end; gap: 8mm; border-bottom: 2px solid #2f6a0d; padding-bottom: 2mm; }
|
||||
.pname { font-size: 24pt; font-weight: 800; line-height: 1.05; letter-spacing: .01em; overflow-wrap: anywhere; }
|
||||
.phead.small .pname { font-size: 15pt; }
|
||||
.pbrand { text-align: right; font-weight: 800; text-transform: uppercase; letter-spacing: .12em; font-size: 8.5pt; color: #2f6a0d !important; white-space: nowrap; }
|
||||
.pbrand span { display: block; font-weight: 500; letter-spacing: .08em; font-size: 7pt; color: #6a7260 !important; }
|
||||
.pfields { display: grid; grid-template-columns: 1fr 1.6fr 1.3fr 1fr .8fr; gap: 3mm; }
|
||||
.pfield { border-bottom: 1px solid #1a1f14; padding-bottom: .6mm; min-width: 0; }
|
||||
.pfield span { display: block; font-size: 6.2pt; text-transform: uppercase; letter-spacing: .1em; color: #6a7260 !important; }
|
||||
.pfield b { font-weight: 600; font-size: 9pt; overflow-wrap: anywhere; }
|
||||
.pattrs { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2.5mm; }
|
||||
.pattr { border: 1.4px solid #1a1f14; border-radius: 3mm; text-align: center; padding: 1.6mm 1mm 1.4mm; display: grid; gap: .2mm; }
|
||||
.pattr-n { font-size: 6.6pt; text-transform: uppercase; letter-spacing: .08em; font-weight: 700; }
|
||||
.pattr-v { font-size: 20pt; line-height: 1; }
|
||||
.pattr-m { font-size: 7pt; color: #4d5645 !important; border-top: 1px solid #cfd5c3; padding-top: .8mm; }
|
||||
.pderived { display: grid; grid-template-columns: repeat(auto-fit, minmax(28mm, 1fr)); gap: 2.5mm; }
|
||||
.pstat { background: #eef3e6; border-radius: 2mm; padding: 1.4mm 2.4mm; display: flex; justify-content: space-between; align-items: baseline; font-weight: 600; font-size: 8pt; }
|
||||
.pstat b { font-size: 13pt; }
|
||||
.psec h3 { margin: 0 0 1.6mm; font-size: 9pt; text-transform: uppercase; letter-spacing: .12em; color: #fff !important; background: #2f6a0d; padding: .8mm 2.4mm; border-radius: 1.4mm; display: flex; justify-content: space-between; align-items: center; gap: 3mm; }
|
||||
.psec h3 * { color: #fff !important; }
|
||||
.psec h3 small { font-size: 6.6pt; font-weight: 500; letter-spacing: .04em; text-transform: none; }
|
||||
.pskills { column-count: 2; column-gap: 6mm; }
|
||||
.pgroup { break-inside: avoid; margin-bottom: 3mm; }
|
||||
.pgroup h4 { margin: 0 0 .6mm; font-size: 7.4pt; text-transform: uppercase; letter-spacing: .1em; color: #2f6a0d !important; border-bottom: 1px solid #2f6a0d; }
|
||||
.prow { display: grid; grid-template-columns: 6.5mm 1fr 11mm 6mm 8mm; align-items: center; gap: 1mm; padding: .35mm 0; border-bottom: 1px dotted #c3cab6; }
|
||||
.prow .pn { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
||||
.prow .pn em { font-size: 6pt; color: #6a7260 !important; font-style: normal; text-transform: uppercase; letter-spacing: .05em; margin-left: .8mm; }
|
||||
.prow .pa { font-size: 6.4pt; color: #6a7260 !important; text-align: center; }
|
||||
.prow .pp { font-size: 7pt; color: #4d5645 !important; text-align: right; }
|
||||
.prow .pt { text-align: center; border: 1px solid #1a1f14; border-radius: 1.4mm; font-size: 8.6pt; padding: .1mm 0; background: #fff; }
|
||||
.prow.locked { opacity: .45; }
|
||||
.prow.locked .pn { text-decoration: line-through; }
|
||||
.ppips { display: inline-flex; gap: .7mm; }
|
||||
.ppips i { width: 2.1mm; height: 2.1mm; border: 1px solid #1a1f14; border-radius: 50%; background: #fff; display: inline-block; }
|
||||
.ppips i.on { background: #2f6a0d; border-color: #2f6a0d; }
|
||||
.pcols { column-count: 2; column-gap: 6mm; }
|
||||
.pitem { break-inside: avoid; margin-bottom: 2.2mm; }
|
||||
.pitem b { font-size: 9pt; }
|
||||
.pitem p { font-size: 7.8pt; color: #3b4433 !important; }
|
||||
.ptag { font-size: 6.6pt; color: #6a7260 !important; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.pnone { color: #6a7260 !important; }
|
||||
.pgear { width: 100%; border-collapse: collapse; font-size: 8pt; }
|
||||
.pgear th { text-align: left; font-size: 6.4pt; text-transform: uppercase; letter-spacing: .08em; color: #6a7260 !important; border-bottom: 1px solid #1a1f14; padding: .6mm 1mm; }
|
||||
.pgear td { padding: .45mm 1mm; border-bottom: 1px dotted #c3cab6; vertical-align: top; }
|
||||
.pgear td:nth-child(n+4), .pgear th:nth-child(n+4) { text-align: right; white-space: nowrap; }
|
||||
.pnote { font-size: 6.6pt; color: #5c6553 !important; }
|
||||
.pother { margin-top: 1.6mm; font-size: 8pt; }
|
||||
.pbg { font-size: 8pt; white-space: pre-wrap; }
|
||||
.pnotes { flex: 1; display: flex; flex-direction: column; min-height: 18mm; }
|
||||
.plines { flex: 1; background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 6.4mm, #b9c1ab 6.4mm, #b9c1ab calc(6.4mm + 1px)); border-radius: 1mm; }
|
||||
.pfoot { margin-top: auto; display: flex; justify-content: space-between; font-size: 6.4pt; color: #6a7260 !important; text-transform: uppercase; letter-spacing: .08em; border-top: 1px solid #cfd5c3; padding-top: 1.2mm; }
|
||||
@media print { .paper { box-shadow: none; } }
|
||||
|
||||
Reference in New Issue
Block a user