/** * Boletim renderer (C186): one A4 `.sheet` in voter-facing language. Ports the * approved hi-fi classes (`dossie-solla-cidade-boletim-ui-design.html`). No * source list — the facts come from the dossiê ledger, and the page is labelled * as an internal model. */ import { isSubjectUnit } from './dossieUnit.mjs' import { htmlEscape, moreItemsLabel } from './reportText.mjs' const ASSET = (title, lines) => `

${htmlEscape(title)}

${lines .map((line) => htmlEscape(line)) .join('
')}

` const phaseModifier = (phase) => phase === 'pago' || phase === 'liquidado' ? 'phase-paid' : phase === 'empenhado' || phase === 'autorizado' ? 'phase-pending' : '' const renderHighlight = (highlight, { showPhase = false } = {}) => `

${htmlEscape(highlight.eyebrow)}

${!highlight.textual && highlight.number ? `

${htmlEscape(highlight.number)}

` : ''} ${ showPhase && highlight.number && highlight.phaseLabel ? `

${htmlEscape(highlight.phaseLabel)}

` : '' }

${htmlEscape(highlight.title)}

${highlight.note ? `

${htmlEscape(highlight.note)}

` : ''}
` const renderMoreItem = (item) => `

${htmlEscape(item.label)}

` const PRINT_CSS = ` * { box-sizing: border-box; } html, body { margin: 0; padding: 0; } body { font-family: 'Fira Sans', 'DejaVu Sans', Arial, sans-serif; color: #172033; background: #fff; } @page { size: A4; margin: 0; } .sheet { position: relative; width: 210mm; height: 297mm; min-height: 297mm; padding: 8mm 10mm 7mm 12mm; display: flex; flex-direction: column; overflow: hidden; background: #fff; font-size: 10.25pt; line-height: 1.26; } .sheet::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 3.5mm; background: #315c75; } .sheet p, .sheet h1, .sheet h2, .sheet h3, .sheet ul { margin-top: 0; } .tabular { font-variant-numeric: tabular-nums; } .model-label { display: inline-flex; align-items: center; min-height: 6mm; padding: 1mm 2.5mm; border: .35mm solid #8a5a18; border-radius: 99px; color: #68420f; background: #fff7e8; font-size: 7.3pt; line-height: 1; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } .kicker, .section-label { color: #315c75; font-size: 7.4pt; line-height: 1.1; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; } .section-label { margin-bottom: 1.2mm; } .asset-box { border: .4mm dashed #748894; color: #40535f; background: #f7f9fa; padding: 3mm; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } .asset-box p { margin: 0; } .asset-box .asset-title { font-size: 7.5pt; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; } .asset-box p + p { margin-top: 1mm; font-size: 7pt; line-height: 1.25; } .bulletin-head { display: flex; align-items: center; justify-content: space-between; gap: 6mm; border-bottom: .55mm solid #172033; padding-bottom: 2.5mm; } .bulletin-head .kicker { margin-bottom: 1mm; } .bulletin-head .place { margin: 0; font-size: 10pt; font-weight: 700; color: #435264; } .bulletin-head .note { margin: 0; max-width: 44mm; text-align: right; font-size: 6.8pt; font-weight: 600; line-height: 1.2; color: #8a5a18; } .bulletin-head .right { display: flex; align-items: center; gap: 3mm; } .opening { margin-top: 3.5mm; display: grid; grid-template-columns: 1fr 46mm; gap: 5mm; } .opening h1 { margin: 0; font-size: 23pt; font-weight: 600; line-height: 1; letter-spacing: -.038em; } .opening h1 .accent { color: #315c75; } .opening .lead { margin: 2.6mm 0 0; max-width: 125mm; font-size: 10.5pt; line-height: 1.33; color: #435264; } .opening .asset-box { min-height: 36mm; border-radius: .125rem; } .highlights { margin-top: 3.8mm; } .highlights-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 4mm; border-bottom: .3mm solid #cbd5dc; padding-bottom: 1.5mm; } .highlights-head h2 { margin: 0; font-size: 14pt; font-weight: 600; line-height: 1; letter-spacing: -.015em; } .highlights-head .side { margin: 0; text-align: right; font-size: 7pt; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; color: #435264; } .highlight-grid { margin-top: 2.5mm; display: grid; grid-template-columns: repeat(3, 1fr); gap: 2.2mm; } .highlight-card { min-height: 31mm; padding: 2.8mm 3mm 2.5mm; border: .3mm solid #cbd5dc; border-top: 1.15mm solid #315c75; background: #fff; } .highlight-eyebrow { margin-bottom: 1.5mm; color: #435264; font-size: 6.7pt; line-height: 1; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } .highlight-number { margin: 0; color: #315c75; font-size: 18.5pt; line-height: .96; font-weight: 800; letter-spacing: -.035em; } .highlight-title { margin: 1.5mm 0 0; font-size: 10.8pt; line-height: 1.08; font-weight: 800; letter-spacing: -.01em; } .highlight-note { margin: 1.3mm 0 0; color: #435264; font-size: 8.15pt; line-height: 1.22; } .bulletin-timeline { margin-top: 3.5mm; border-radius: .125rem; background: #e8f0f4; padding: 2.8mm 3mm 2.6mm; } .bulletin-timeline .timeline-head { display: flex; align-items: center; justify-content: space-between; gap: 4mm; } .bulletin-timeline h2 { margin: 0; font-size: 11pt; font-weight: 600; } .bulletin-timeline .timeline-side { margin: 0; font-size: 6.7pt; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: #315c75; } .timeline-grid { margin-top: 3mm; display: grid; grid-template-columns: repeat(4, 1fr); } .timeline-step { position: relative; padding: 4.5mm 2.5mm 1.5mm; border-top: .4mm solid #97aab6; } .timeline-step::before { content: ''; position: absolute; top: -1.6mm; left: 2.5mm; width: 3mm; height: 3mm; border: .65mm solid #e8f0f4; border-radius: 99px; background: #315c75; box-shadow: 0 0 0 .3mm #315c75; } .timeline-period { margin: 0 0 .7mm; font-size: 7.2pt; font-weight: 800; color: #315c75; } .timeline-text { margin: 0; font-size: 8.4pt; font-weight: 600; line-height: 1.18; } .more { margin-top: 3.5mm; display: grid; grid-template-columns: 1fr 43mm; gap: 4mm; } .more-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 4mm; border-bottom: .3mm solid #cbd5dc; padding-bottom: 1.4mm; } .more-head h2 { margin: 0; font-size: 12.5pt; font-weight: 600; line-height: 1; } .more-head .side { margin: 0; font-size: 6.7pt; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; color: #435264; } .more-grid { margin-top: 2.3mm; display: grid; grid-template-columns: 1fr 1fr; gap: 1.7mm 4mm; } .more-item { position: relative; margin: 0; min-height: 11mm; padding: 0 0 2mm 5mm; border-bottom: .25mm solid #cbd5dc; font-size: 9pt; line-height: 1.22; } .more-item::before { content: '✓'; position: absolute; left: 0; top: 0; color: #315c75; font-weight: 900; } .more-counter { margin-top: 1.8mm; color: #435264; font-size: 7.6pt; font-weight: 600; } .more-aside { display: grid; grid-template-rows: 1fr 1fr; gap: 2.3mm; } .more-aside .asset-box { border-radius: .125rem; } .empty { margin-top: 3mm; color: #435264; font-size: 9.5pt; } .empty-box { margin-top: 2.3mm; min-height: 40mm; border: .4mm dashed #748894; background: #f7f9fa; color: #40535f; padding: 4mm; display: flex; flex-direction: column; justify-content: center; gap: 1.5mm; } .empty-box p { margin: 0; font-size: 9.5pt; line-height: 1.3; } .empty-box .uppercase { text-transform: uppercase; letter-spacing: .06em; } .bulletin-foot { margin-top: auto; border-top: .35mm solid #cbd5dc; padding-top: 2.2mm; } .bulletin-foot .row { display: flex; align-items: flex-end; justify-content: space-between; gap: 5mm; } .bulletin-foot .orig { margin: 0 0 .8mm; font-size: 7.2pt; font-weight: 600; line-height: 1.25; color: #315c75; } .bulletin-foot .defeso { margin: 0; max-width: 142mm; font-size: 7.4pt; line-height: 1.28; color: #435264; } .bulletin-foot .defeso strong { color: #8a5a18; } .bulletin-foot .stamp { margin: 0; text-align: right; font-size: 7.2pt; font-weight: 600; line-height: 1.3; color: #435264; } ` const renderMunicipalityBulletinHtml = (bulletin) => ` ${htmlEscape(bulletin.meta.title)} — ${htmlEscape(bulletin.meta.municipality)}

${htmlEscape(bulletin.meta.kicker)}

Município de ${htmlEscape(bulletin.meta.municipality)}${bulletin.meta.region ? ` · ${htmlEscape(bulletin.meta.region)}` : ' · Bahia'}

Síntese do dossiê da cidade — conferir antes de publicar.

${htmlEscape(bulletin.meta.modelLabel)}

O que Jorge Solla fez por ${htmlEscape(bulletin.meta.municipality)}

Da gestão da saúde ao trabalho na Câmara: recursos, serviços, obras e equipamentos que melhoram a vida de quem mora aqui.

${ASSET('NEEDS ASSET · foto real', ['Solla na cidade ou no serviço citado', 'origem, autoria e licença'])}

Entregas que fazem diferença

cidade ≠ região · cada alcance vem identificado

${ bulletin.highlights.length ? `
${bulletin.highlights.map(renderHighlight).join('')}
` : '

Sem fato com fonte suficiente para destacar — ver lacunas no dossiê.

' }

Desde 1999, uma trajetória na saúde pública

experiência que virou trabalho pela Bahia

${bulletin.timeline .map( (step) => `

${htmlEscape(step.period)}

${htmlEscape(step.text)}

`, ) .join('')}

Outras ações que entram no boletim

frases curtas · fatos confirmados

${ bulletin.moreItems.length ? `
${bulletin.moreItems.map(renderMoreItem).join('')}
` : '

Sem itens adicionais com fonte além dos destaques.

Conferir as lacunas explícitas no dossiê da cidade antes de ampliar o boletim.

' } ${ bulletin.factsRemaining > 0 ? `

${htmlEscape(moreItemsLabel(bulletin.factsRemaining, 'fato com fonte', 'fatos com fonte'))} no dossiê da cidade — o boletim de uma página não os exibe.

` : '' }
` /* Institution boletim (C187) — ports the institutional boletim hi-fi. */ const SUBJECT_BULLETIN_CSS = ` ${PRINT_CSS} .defeso-band { margin-inline: -10mm; padding: 1.4mm 10mm 1.35mm; display: flex; align-items: center; justify-content: space-between; gap: 5mm; color: #68420f; background: #fff7e8; border-top: .25mm solid #e7cc9d; border-bottom: .25mm solid #e7cc9d; font-size: 7.15pt; line-height: 1.2; font-weight: 700; } .identity-pill { display: inline-flex; align-items: center; min-height: 5mm; padding: .55mm 1.7mm; border: .25mm solid #cbd5dc; border-radius: 99px; color: #435264; background: #f3f6f8; font-size: 6.9pt; line-height: 1; font-weight: 700; letter-spacing: .045em; text-transform: uppercase; } .identity-pill--code { font-family: ui-monospace, 'DejaVu Sans Mono', monospace; letter-spacing: .02em; } .bulletin-head .subject { margin: 0; font-size: 11pt; font-weight: 800; } .bulletin-head .pills { margin-top: 1.3mm; display: flex; flex-wrap: wrap; gap: 1.2mm; } .lacuna-panel { border: .4mm dashed #b18549; color: #684819; background: #fff7e8; } .lacuna-panel .lacuna-rule { margin-top: 2mm; border-top: .25mm solid #d8bb8d; padding-top: 2mm; font-size: 7.5pt; font-weight: 700; text-transform: uppercase; line-height: 1.35; letter-spacing: .055em; } .production-note { border: .25mm solid #cbd5dc; background: #f3f6f8; border-radius: .125rem; padding: 3mm; } .opening { margin-top: 2.5mm; } .highlights { margin-top: 3mm; } .bulletin-timeline { margin-top: 2.5mm; } .more { margin-top: 2.5mm; } .empty-box { min-height: 30mm; } .more-counter { margin-top: 1.8mm; color: #435264; font-size: 7.6pt; font-weight: 600; } .lacuna-panel .warn-icon { width: 5mm; height: 5mm; flex-shrink: 0; margin-top: .4mm; color: #8a5a18; } .highlight-phase { margin: 1.2mm 0 0; } .highlight-card--textual { border-top-color: #435264; } .highlight-card--textual .highlight-title { margin-top: 0; font-size: 11.6pt; } .phase { display: inline-block; padding: .5mm 1.4mm; border-radius: 1mm; background: #e9eef1; color: #334553; font-size: 7.3pt; font-weight: 700; text-transform: uppercase; letter-spacing: .035em; } .phase-paid { color: #285338; background: #e6f2e9; } .phase-pending { color: #6b4918; background: #fff2d9; } ` const INST_WARNING_ICON = '' const renderSubjectBulletinHtml = (bulletin) => { const sparse = Boolean(bulletin.sparse) const copy = bulletin.unit.copy.bulletin // Theme pills come from the descriptor (`Área` + canonical token + taxonomy); // institution keeps the identity badges untouched. const pillList = bulletin.meta.identityPills ? bulletin.meta.identityPills.filter((pill) => Boolean(pill.label)) : (bulletin.meta.identityBadges ?? []).map((badge) => ({ label: badge })) const pills = pillList .map( (pill) => `${htmlEscape(pill.label)}`, ) .join('') const highlights = bulletin.highlights const more = bulletin.moreItems return ` ${htmlEscape(bulletin.meta.title)} — ${htmlEscape(bulletin.meta.subjectName)}

${htmlEscape(bulletin.meta.kicker)}

${htmlEscape(bulletin.meta.subjectName)}

${pills ? `
${pills}
` : ''}
${htmlEscape(bulletin.meta.modelLabel)}
DEFESO 2026 · material em preparação · não publicar sem revisão editorial e jurídica sem CTA · sem propaganda

${(() => { const title = copy.openingTitle(bulletin.meta.subjectName) const name = htmlEscape(bulletin.meta.subjectName) const escaped = htmlEscape(title) return escaped.includes(name) ? escaped.replace(name, `${name}`) : escaped })()}

${sparse ? htmlEscape(copy.leadSparse) : htmlEscape(copy.lead)}

${ASSET(sparse ? 'NEEDS ASSET · foto ou clipping' : 'NEEDS ASSET · foto real', [ sparse ? copy.assetSparse : copy.asset, 'origem, autoria e licença', ])}

${sparse ? htmlEscape(copy.highlightsTitleSparse) : htmlEscape(copy.highlightsTitle)}

${sparse ? 'não criar cards vazios para chegar a seis' : 'cada número mantém sua fase e seu alcance'}

${ highlights.length ? `
${highlights.map((highlight) => renderHighlight(highlight, { showPhase: true })).join('')}
` : '

Sem fato com fonte suficiente para destacar — ver lacunas no dossiê.

' }

${htmlEscape(copy.title)}

${sparse ? htmlEscape(copy.titleSparse) : 'quatro momentos · linguagem direta'}

${bulletin.timeline .map( (step) => `

${htmlEscape(step.period)}

${htmlEscape(step.text)}

`, ) .join('')}

${sparse ? htmlEscape(copy.moreTitleSparse) : htmlEscape(copy.moreTitle)}

${sparse ? 'sem completar por inferência' : 'até 14 · sem completar por inferência'}

${ more.length ? `
${more.map(renderMoreItem).join('')}
` : `

Sem itens adicionais com fonte além dos destaques.

${htmlEscape(copy.moreEmpty)}

` } ${ bulletin.factsRemaining > 0 ? `

${htmlEscape(moreItemsLabel(bulletin.factsRemaining, 'fato com fonte', 'fatos com fonte'))} ${htmlEscape(copy.remaining)}

` : '' }
${ sparse ? `` : `` }
${ sparse ? `

Se novos fatos forem documentados, eles entram por relevância — até seis destaques e até quatorze itens. A ausência nunca vira número zero, percentual ou afirmação genérica.

${ASSET('NEEDS ASSET · selo', copy.assetSelo ?? [])}
` : '' }
` } /** Dispatch: subject-shaped units (institution/theme) render the shared subject composition. */ export const renderBulletinHtml = (bulletin) => isSubjectUnit(bulletin?.unit) ? renderSubjectBulletinHtml(bulletin) : renderMunicipalityBulletinHtml(bulletin)