diff --git a/internal/db/migrations/150_submission_bases_specialist.down.sql b/internal/db/migrations/150_submission_bases_specialist.down.sql
new file mode 100644
index 0000000..798acb2
--- /dev/null
+++ b/internal/db/migrations/150_submission_bases_specialist.down.sql
@@ -0,0 +1,3 @@
+-- t-paliad-317: revert specialist base seed rows.
+
+DELETE FROM paliad.submission_bases WHERE slug IN ('lg-duesseldorf', 'upc-formal');
diff --git a/internal/db/migrations/150_submission_bases_specialist.up.sql b/internal/db/migrations/150_submission_bases_specialist.up.sql
new file mode 100644
index 0000000..195d379
--- /dev/null
+++ b/internal/db/migrations/150_submission_bases_specialist.up.sql
@@ -0,0 +1,128 @@
+-- t-paliad-317 (m/paliad#141): Composer Slice E — specialist bases.
+--
+-- Two firm-agnostic bases for proceeding-family-specific styling:
+--
+-- lg-duesseldorf — DE LG (de.inf.lg) conservative German legal style.
+-- Times New Roman 11pt; black headings.
+-- upc-formal — UPC court of first instance (upc.inf.cfi) formal
+-- style. Calibri 11pt body; UPC-blue (1F3864) headings;
+-- Cambria italic for blockquotes.
+--
+-- The .docx body for each is a minimal Composer-mode skeleton with
+-- the 10 default section anchors and an empty rels envelope. The
+-- styles.xml declares the {prefix}-Body / -Heading1/2/3 / -ListBullet
+-- / -ListNumber / -Quote paragraph styles + a "Hyperlink" character
+-- style (matches the MD walker's emitted r:id="rIdComposerN" link
+-- runs from Slice D).
+--
+-- Generator: scripts/gen-submission-base/main.go (each preset hard-
+-- codes the typography). The .docx files are uploaded to Gitea at
+-- 6 - material/Templates/Word/Paliad/Composer/{slug}.docx as mAi.
+--
+-- The mig is additive only: ON CONFLICT (slug) DO NOTHING keeps a
+-- re-run safe and existing rows untouched.
+
+INSERT INTO paliad.submission_bases
+ (slug, firm, proceeding_family, label_de, label_en,
+ description_de, description_en,
+ gitea_path, section_spec, is_default_for)
+VALUES
+ ('lg-duesseldorf', NULL, 'de.inf.lg',
+ 'LG-Düsseldorf-Stil', 'LG-Düsseldorf style',
+ 'Konservativer DE-LG-Stil: Times New Roman 11pt, schlichte Überschriften.',
+ 'Conservative DE LG style: Times New Roman 11pt, plain headings.',
+ '6 - material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx',
+ jsonb_build_object(
+ 'version', 1,
+ 'stylemap', jsonb_build_object(
+ 'paragraph', 'LG-Body',
+ 'heading_1', 'LG-Heading1',
+ 'heading_2', 'LG-Heading2',
+ 'heading_3', 'LG-Heading3',
+ 'list_bullet', 'LG-ListBullet',
+ 'list_numbered', 'LG-ListNumber',
+ 'blockquote', 'LG-Quote'
+ ),
+ 'defaults', jsonb_build_array(
+ jsonb_build_object('section_key','letterhead', 'kind','prose', 'order_index', 1, 'label_de','Briefkopf', 'label_en','Letterhead',
+ 'included',true,
+ 'seed_md_de', E'Schriftsatz von {{firm.name}}\n\n{{user.display_name}}',
+ 'seed_md_en', E'Submission by {{firm.name}}\n\n{{user.display_name}}'),
+ jsonb_build_object('section_key','caption', 'kind','prose', 'order_index', 2, 'label_de','Rubrum', 'label_en','Caption',
+ 'included',true,
+ 'seed_md_de', E'In der Sache\n\n**{{parties.claimant.0.name}}**\n— Klägerin —\n\ngegen\n\n**{{parties.defendant.0.name}}**\n— Beklagte —\n\nAktenzeichen: {{project.case_number}}\n{{project.court}}',
+ 'seed_md_en', E'In the matter\n\n**{{parties.claimant.0.name}}**\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\n— Defendant —\n\nCase number: {{project.case_number}}\n{{project.court}}'),
+ jsonb_build_object('section_key','introduction', 'kind','prose', 'order_index', 3, 'label_de','Einleitung', 'label_en','Introduction',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','requests', 'kind','requests', 'order_index', 4, 'label_de','Anträge', 'label_en','Requests',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','facts', 'kind','prose', 'order_index', 5, 'label_de','Sachverhalt', 'label_en','Facts',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','legal_argument', 'kind','prose', 'order_index', 6, 'label_de','Rechtliche Würdigung', 'label_en','Legal argument',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','evidence', 'kind','evidence', 'order_index', 7, 'label_de','Beweisangebote', 'label_en','Evidence offering',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','exhibits', 'kind','prose', 'order_index', 8, 'label_de','Anlagen', 'label_en','Exhibits',
+ 'included',false, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','closing', 'kind','prose', 'order_index', 9, 'label_de','Schlussformel', 'label_en','Closing',
+ 'included',true,
+ 'seed_md_de', E'Mit freundlichen Grüßen',
+ 'seed_md_en', E'Yours sincerely,'),
+ jsonb_build_object('section_key','signature', 'kind','prose', 'order_index',10, 'label_de','Unterschrift', 'label_en','Signature',
+ 'included',true,
+ 'seed_md_de', E'{{user.display_name}}',
+ 'seed_md_en', E'{{user.display_name}}')
+ )
+ ),
+ '{}'::text[]
+ ),
+ ('upc-formal', NULL, 'upc.inf.cfi',
+ 'UPC-Verfahren', 'UPC formal',
+ 'UPC-Verfahrensstil: Calibri 11pt, UPC-blaue Überschriften, Cambria-Zitate.',
+ 'UPC court style: Calibri 11pt, UPC-blue headings, Cambria quotes.',
+ '6 - material/Templates/Word/Paliad/Composer/upc-formal.docx',
+ jsonb_build_object(
+ 'version', 1,
+ 'stylemap', jsonb_build_object(
+ 'paragraph', 'UPC-Body',
+ 'heading_1', 'UPC-Heading1',
+ 'heading_2', 'UPC-Heading2',
+ 'heading_3', 'UPC-Heading3',
+ 'list_bullet', 'UPC-ListBullet',
+ 'list_numbered', 'UPC-ListNumber',
+ 'blockquote', 'UPC-Quote'
+ ),
+ 'defaults', jsonb_build_array(
+ jsonb_build_object('section_key','letterhead', 'kind','prose', 'order_index', 1, 'label_de','Briefkopf', 'label_en','Letterhead',
+ 'included',true,
+ 'seed_md_de', E'Schriftsatz von {{firm.name}}\n\n{{user.display_name}}',
+ 'seed_md_en', E'Submission by {{firm.name}}\n\n{{user.display_name}}'),
+ jsonb_build_object('section_key','caption', 'kind','prose', 'order_index', 2, 'label_de','Rubrum', 'label_en','Caption',
+ 'included',true,
+ 'seed_md_de', E'# In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n— Defendant —\n\nUPC-Aktenzeichen: {{project.case_number}}\nStreitpatent: {{project.patent_number_upc}}',
+ 'seed_md_en', E'# In the matter\n\n**{{parties.claimant.0.name}}**\nrepresented by {{parties.claimant.0.representative}}\n— Claimant —\n\nv.\n\n**{{parties.defendant.0.name}}**\nrepresented by {{parties.defendant.0.representative}}\n— Defendant —\n\nUPC case number: {{project.case_number}}\nPatent in suit: {{project.patent_number_upc}}'),
+ jsonb_build_object('section_key','introduction', 'kind','prose', 'order_index', 3, 'label_de','Einleitung', 'label_en','Introduction',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','requests', 'kind','requests', 'order_index', 4, 'label_de','Anträge', 'label_en','Requests',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','facts', 'kind','prose', 'order_index', 5, 'label_de','Sachverhalt', 'label_en','Facts',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','legal_argument', 'kind','prose', 'order_index', 6, 'label_de','Rechtliche Würdigung', 'label_en','Legal argument',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','evidence', 'kind','evidence', 'order_index', 7, 'label_de','Beweisangebote', 'label_en','Evidence offering',
+ 'included',true, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','exhibits', 'kind','prose', 'order_index', 8, 'label_de','Anlagen', 'label_en','Exhibits',
+ 'included',false, 'seed_md_de', '', 'seed_md_en', ''),
+ jsonb_build_object('section_key','closing', 'kind','prose', 'order_index', 9, 'label_de','Schlussformel', 'label_en','Closing',
+ 'included',true,
+ 'seed_md_de', E'Mit freundlichen Grüßen',
+ 'seed_md_en', E'Yours sincerely,'),
+ jsonb_build_object('section_key','signature', 'kind','prose', 'order_index',10, 'label_de','Unterschrift', 'label_en','Signature',
+ 'included',true,
+ 'seed_md_de', E'{{user.display_name}}',
+ 'seed_md_en', E'{{user.display_name}}')
+ )
+ ),
+ '{}'::text[]
+ )
+ON CONFLICT (slug) DO NOTHING;
diff --git a/internal/handlers/files.go b/internal/handlers/files.go
index 0faabe3..ebb2aba 100644
--- a/internal/handlers/files.go
+++ b/internal/handlers/files.go
@@ -113,8 +113,34 @@ var fileRegistry = map[string]fileEntry{
RepoName: "mWorkRepo",
FilePath: "6 - material/Templates/Word/Paliad/" + branding.Name + "/_skeleton.en.docx",
},
+ // t-paliad-317 Composer Slice E — specialist firm-agnostic bases.
+ // Both live under Composer/ (not under HLC/) so a future non-HLC
+ // deployment serves the same cross-firm files. Body = anchor-only
+ // per Slice B; styles.xml carries the preset's typography.
+ composerBaseLGDuesseldorfSlug: {
+ RawURL: "https://mgit.msbls.de/m/mWorkRepo/raw/branch/main/6%20-%20material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx",
+ DownloadName: "LG-Düsseldorf Stil.docx",
+ ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ RepoOwner: "m",
+ RepoName: "mWorkRepo",
+ FilePath: "6 - material/Templates/Word/Paliad/Composer/lg-duesseldorf.docx",
+ },
+ composerBaseUPCFormalSlug: {
+ RawURL: "https://mgit.msbls.de/m/mWorkRepo/raw/branch/main/6%20-%20material/Templates/Word/Paliad/Composer/upc-formal.docx",
+ DownloadName: "UPC formal.docx",
+ ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ RepoOwner: "m",
+ RepoName: "mWorkRepo",
+ FilePath: "6 - material/Templates/Word/Paliad/Composer/upc-formal.docx",
+ },
}
+// t-paliad-317 Composer Slice E — slugs for the new specialist bases.
+const (
+ composerBaseLGDuesseldorfSlug = "submission/composer/lg-duesseldorf.docx"
+ composerBaseUPCFormalSlug = "submission/composer/upc-formal.docx"
+)
+
// skeletonSubmissionSlug names the universal skeleton template inside
// the shared fileRegistry cache. Exported via a const so handler code
// (resolveSubmissionTemplate, hlPatentsStyleSHA's sibling) refers to
@@ -413,6 +439,8 @@ func fetchFirmSkeletonBytes(ctx context.Context) ([]byte, string, error) {
var composerBaseSlugMap = map[string]string{
"hlc-letterhead": firmSkeletonSubmissionSlug,
"neutral": skeletonSubmissionSlug,
+ "lg-duesseldorf": composerBaseLGDuesseldorfSlug,
+ "upc-formal": composerBaseUPCFormalSlug,
}
// fetchComposerBaseBytes returns the .docx bytes for a Composer base,
diff --git a/internal/services/submission_compose_test.go b/internal/services/submission_compose_test.go
index 62b5b86..6d310e0 100644
--- a/internal/services/submission_compose_test.go
+++ b/internal/services/submission_compose_test.go
@@ -368,6 +368,89 @@ func TestComposer_HyperlinkDedupesByURL(t *testing.T) {
}
}
+// Slice E — base swap preserves section content; only chrome / styles
+// change. This is the design's "Markdown is base-agnostic" contract
+// from Q10 + §5.3 ratification. We compose the SAME section text
+// against two bases with DIFFERENT stylemaps and verify:
+// 1. The section text appears in both outputs.
+// 2. Each base applies its OWN paragraph style (the stylemap diff
+// is the only visible delta in the document body).
+
+func TestComposer_BaseSwapPreservesContent(t *testing.T) {
+ body := `{{#section:facts}}{{/section:facts}}`
+ baseBytes := minimalBaseBytes(t, body)
+
+ // Base A: HLC-style stylemap.
+ hlc := &SubmissionBase{
+ ID: uuid.New(), Slug: "hlc-test",
+ SectionSpec: BaseSectionSpec{
+ Stylemap: map[string]string{
+ "paragraph": "HLpat-Body-B0",
+ "heading_1": "HLpat-Heading-H1",
+ },
+ },
+ }
+ // Base B: LG-style stylemap.
+ lg := &SubmissionBase{
+ ID: uuid.New(), Slug: "lg-test",
+ SectionSpec: BaseSectionSpec{
+ Stylemap: map[string]string{
+ "paragraph": "LG-Body",
+ "heading_1": "LG-Heading1",
+ },
+ },
+ }
+
+ // Identical Markdown content rendered against each base.
+ md := "# Heading line\n\nA paragraph of substantive prose."
+ sections := []SubmissionSection{
+ {ID: uuid.New(), SectionKey: "facts", OrderIndex: 1, Kind: "prose", Included: true, ContentMDDE: md},
+ }
+
+ composer := NewSubmissionComposer(NewSubmissionRenderer())
+ hlcOut, err := composer.Compose(context.Background(), ComposeOptions{
+ Sections: sections, Base: hlc, BaseBytes: baseBytes, Lang: "de",
+ })
+ if err != nil {
+ t.Fatalf("Compose hlc: %v", err)
+ }
+ lgOut, err := composer.Compose(context.Background(), ComposeOptions{
+ Sections: sections, Base: lg, BaseBytes: baseBytes, Lang: "de",
+ })
+ if err != nil {
+ t.Fatalf("Compose lg: %v", err)
+ }
+
+ hlcXML := extractDocumentXML(t, hlcOut)
+ lgXML := extractDocumentXML(t, lgOut)
+
+ // Content survives both ways.
+ for _, want := range []string{"Heading line", "A paragraph of substantive prose."} {
+ if !strings.Contains(hlcXML, want) {
+ t.Errorf("HLC output missing content %q", want)
+ }
+ if !strings.Contains(lgXML, want) {
+ t.Errorf("LG output missing content %q", want)
+ }
+ }
+
+ // Stylemap diff actually shows up in the body — HLC's headings
+ // use HLpat-Heading-H1, LG's use LG-Heading1. If the composer
+ // silently passed the wrong stylemap, this would fire.
+ if !strings.Contains(hlcXML, ``) {
+ t.Errorf("HLC heading style missing: %s", hlcXML)
+ }
+ if !strings.Contains(lgXML, ``) {
+ t.Errorf("LG heading style missing: %s", lgXML)
+ }
+ if strings.Contains(hlcXML, ``) {
+ t.Errorf("HLC output leaked LG style: %s", hlcXML)
+ }
+ if strings.Contains(lgXML, ``) {
+ t.Errorf("LG output leaked HLC style: %s", lgXML)
+ }
+}
+
func TestComposer_OrderIndexAscending(t *testing.T) {
base := composerBase()
// No anchors → both sections append in order_index ASC order
diff --git a/scripts/gen-submission-base/main.go b/scripts/gen-submission-base/main.go
new file mode 100644
index 0000000..3cce275
--- /dev/null
+++ b/scripts/gen-submission-base/main.go
@@ -0,0 +1,256 @@
+// Composer Slice E base-template generator (t-paliad-317).
+//
+// Produces a minimal Composer-mode .docx whose contains the
+// 10 default section anchors and whose word/styles.xml declares a
+// named style for each stylemap key the composer references. Each
+// "preset" (lg-duesseldorf, upc-formal, …) hard-codes the typography
+// (font, sizes, colour) so the lawyer can swap between them and see
+// the chrome change while the section content carries through
+// unchanged (the Q10 base-swap-content-survival contract).
+//
+// Run:
+//
+// go run ./scripts/gen-submission-base -preset lg-duesseldorf -out /tmp/lg-duesseldorf.docx
+// go run ./scripts/gen-submission-base -preset upc-formal -out /tmp/upc-formal.docx
+//
+// Both outputs are byte-reproducible (zip mtimes pinned to a fixed
+// UTC timestamp so a clean rebuild diff stays at zero bytes).
+//
+// Cross-firm: the bases this generator emits are firm-agnostic
+// (firm = NULL on the catalog row). They contain no HLC branding
+// content. Per-firm bases continue to use gen-hl-skeleton-template
+// against the proprietary .dotm source.
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+)
+
+func main() {
+ preset := flag.String("preset", "", "preset: lg-duesseldorf | upc-formal")
+ out := flag.String("out", "", "output .docx path (required)")
+ flag.Parse()
+
+ if *preset == "" || *out == "" {
+ fmt.Fprintln(os.Stderr, "usage: gen-submission-base -preset NAME -out PATH")
+ os.Exit(2)
+ }
+
+ cfg, ok := presets[*preset]
+ if !ok {
+ fmt.Fprintf(os.Stderr, "unknown preset %q (available: ", *preset)
+ first := true
+ for k := range presets {
+ if !first {
+ fmt.Fprint(os.Stderr, ", ")
+ }
+ fmt.Fprint(os.Stderr, k)
+ first = false
+ }
+ fmt.Fprintln(os.Stderr, ")")
+ os.Exit(2)
+ }
+
+ docx, err := buildDocx(cfg)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "gen-submission-base:", err)
+ os.Exit(1)
+ }
+ if err := os.WriteFile(*out, docx, 0o644); err != nil {
+ fmt.Fprintln(os.Stderr, "gen-submission-base: write:", err)
+ os.Exit(1)
+ }
+ fmt.Printf("wrote %s (%d bytes) for preset %s\n", *out, len(docx), *preset)
+}
+
+// presetConfig captures everything the generator needs to vary between
+// bases: typography defaults (font + size + colour) and the style-name
+// prefix that surfaces in the styles.xml.
+type presetConfig struct {
+ StylePrefix string // e.g. "LG" / "UPC"
+ DefaultFont string // e.g. "Times New Roman" / "Calibri"
+ BodyHalfPoints int // w:sz value (half-points; 22 = 11pt)
+ Heading1Size int
+ Heading2Size int
+ Heading3Size int
+ Heading1Color string // hex without #
+ Heading2Color string
+ Heading3Color string
+ BlockquoteFont string // separate font for the quote style
+}
+
+// presets are the seeded base styles for Slice E. Both are intended
+// as starting points the firm's admin can refine via the admin editor
+// in a later slice — this is the floor, not the ceiling.
+var presets = map[string]presetConfig{
+ "lg-duesseldorf": {
+ StylePrefix: "LG",
+ DefaultFont: "Times New Roman",
+ BodyHalfPoints: 22, // 11pt
+ Heading1Size: 28, // 14pt
+ Heading2Size: 26, // 13pt
+ Heading3Size: 24, // 12pt
+ Heading1Color: "000000",
+ Heading2Color: "000000",
+ Heading3Color: "000000",
+ BlockquoteFont: "Times New Roman",
+ },
+ "upc-formal": {
+ StylePrefix: "UPC",
+ DefaultFont: "Calibri",
+ BodyHalfPoints: 22, // 11pt
+ Heading1Size: 32, // 16pt
+ Heading2Size: 28, // 14pt
+ Heading3Size: 24, // 12pt
+ Heading1Color: "1F3864", // UPC dark blue
+ Heading2Color: "1F3864",
+ Heading3Color: "1F3864",
+ BlockquoteFont: "Cambria",
+ },
+}
+
+var fixedTime = time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
+
+func buildDocx(cfg presetConfig) ([]byte, error) {
+ var buf bytes.Buffer
+ zw := zip.NewWriter(&buf)
+
+ add := func(name, body string) error {
+ hdr := &zip.FileHeader{Name: name, Method: zip.Deflate, Modified: fixedTime}
+ w, err := zw.CreateHeader(hdr)
+ if err != nil {
+ return fmt.Errorf("create %s: %w", name, err)
+ }
+ if _, err := w.Write([]byte(body)); err != nil {
+ return fmt.Errorf("write %s: %w", name, err)
+ }
+ return nil
+ }
+
+ if err := add("[Content_Types].xml", contentTypesXML); err != nil {
+ return nil, err
+ }
+ if err := add("_rels/.rels", rootRelsXML); err != nil {
+ return nil, err
+ }
+ if err := add("word/_rels/document.xml.rels", documentRelsXML); err != nil {
+ return nil, err
+ }
+ if err := add("word/styles.xml", buildStylesXML(cfg)); err != nil {
+ return nil, err
+ }
+ if err := add("word/document.xml", buildDocumentXML()); err != nil {
+ return nil, err
+ }
+
+ if err := zw.Close(); err != nil {
+ return nil, fmt.Errorf("finalise zip: %w", err)
+ }
+ return buf.Bytes(), nil
+}
+
+const contentTypesXML = `
+
+
+
+
+
+`
+
+const rootRelsXML = `
+
+
+`
+
+// documentRelsXML — empty relationships envelope. The composer's
+// hyperlink patch slots fresh
+// rows in here at compose time.
+const documentRelsXML = `
+
+
+`
+
+// buildStylesXML emits the stylemap-aligned named styles. Each style
+// id matches what the catalog row's section_spec.stylemap declares
+// for the corresponding key (paragraph / heading_1/2/3 / list_*
+// / blockquote / Hyperlink).
+//
+// "Hyperlink" is the built-in Word style id the composer's MD walker
+// emits on link-child runs (Slice D). Including it here makes the
+// blue-underline-link rendering land out of the box.
+func buildStylesXML(cfg presetConfig) string {
+ var b strings.Builder
+ b.WriteString(``)
+ b.WriteString(``)
+
+ // Document defaults — sets the body font + size for every paragraph
+ // that doesn't override.
+ fmt.Fprintf(&b, ``,
+ cfg.DefaultFont, cfg.DefaultFont, cfg.DefaultFont, cfg.BodyHalfPoints)
+
+ // Normal — Word's default paragraph style; nothing fancy.
+ b.WriteString(``)
+
+ // Body style — body0 alias for the composer's stylemap.paragraph.
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix)
+
+ // Headings — three levels with descending sizes + colours.
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix, cfg.Heading1Size, cfg.Heading1Color)
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix, cfg.Heading2Size, cfg.Heading2Color)
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix, cfg.Heading3Size, cfg.Heading3Color)
+
+ // List paragraph styles — same indent as body but with hanging
+ // indent so the visible "• " / "N. " prefix from the MD walker
+ // aligns cleanly.
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix)
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix)
+
+ // Blockquote — italic, indented, optional alternative font.
+ fmt.Fprintf(&b, ``,
+ cfg.StylePrefix, cfg.StylePrefix, cfg.BlockquoteFont, cfg.BlockquoteFont)
+
+ // Hyperlink — Word's built-in character-style id matches what the
+ // MD walker emits, so the link runs pick up the colour + underline
+ // automatically.
+ b.WriteString(``)
+
+ b.WriteString(``)
+ return b.String()
+}
+
+// buildDocumentXML emits the composer-mode body — 10 default section
+// anchors in the design's §6.1 order, nothing else.
+func buildDocumentXML() string {
+ var b strings.Builder
+ b.WriteString(``)
+ b.WriteString(``)
+ b.WriteString(``)
+ for _, key := range []string{
+ "letterhead", "caption", "introduction", "requests",
+ "facts", "legal_argument", "evidence", "exhibits",
+ "closing", "signature",
+ } {
+ anchor(&b, "{{#section:"+key+"}}")
+ anchor(&b, "{{/section:"+key+"}}")
+ }
+ b.WriteString(``)
+ return b.String()
+}
+
+func anchor(b *strings.Builder, text string) {
+ b.WriteString(``)
+ b.WriteString(text)
+ b.WriteString(``)
+}