@scalemule/news-publication 0.0.47 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -90,6 +90,48 @@ var EDITORIAL_SECTION_ORDER = [
90
90
  "Schools",
91
91
  "Opinion"
92
92
  ];
93
+ var SECTION_ALIASES = {
94
+ "Business": "Local businesses",
95
+ "Government": "Civic life",
96
+ "Arts & Culture": "Community",
97
+ "Food & drink": "Food & places",
98
+ "Things to do": "Events",
99
+ "Sports": "Community",
100
+ "Education": "Schools",
101
+ "Public Safety": "Civic life",
102
+ "Transit": "Civic life",
103
+ "Environment": "Civic life",
104
+ "Recreation": "Community"
105
+ };
106
+ function resolveSection(item) {
107
+ const explicit = item.section?.trim();
108
+ if (explicit && explicit !== "Local news") {
109
+ return SECTION_ALIASES[explicit] ?? explicit;
110
+ }
111
+ const descriptors = [
112
+ ...(item.tags ?? []).map((t) => t.name.toLowerCase()),
113
+ ...(item.categories ?? []).map((c) => c.name.toLowerCase())
114
+ ];
115
+ for (const desc of descriptors) {
116
+ if (/\b(health|wellness|medical|hospital|clinic|doctor|pharmacy|veterinar|pet care|animal welfare|vaccin)\b/i.test(desc)) return "Health";
117
+ if (/\b(real estate|housing|home|homes|mortgage|tenant|tenants|apartment|apartments|property|bmr)\b/i.test(desc)) return "Real Estate";
118
+ if (/\b(civic|government|council|election|ballot|measure|transit|traffic|police|fire|firefighter|wildfire|safety|policy|supervisor)\b/i.test(desc)) return "Civic life";
119
+ if (/\b(business|retail|commerce|commercial|downtown|store|shop|merchants?|chamber of commerce)\b/i.test(desc)) return "Local businesses";
120
+ if (/\b(event|events|festival|festivals|fair|fairs|concert|concerts|music|workshop|workshops|things to do|exhibition|performance)\b/i.test(desc)) return "Events";
121
+ if (/\b(school|schools|education|student|students|teacher|teachers|campus|college|university)\b/i.test(desc)) return "Schools";
122
+ if (/\b(food|dining|restaurant|restaurants|wine|winery|brewery|bakery|cafe|coffee|cuisine|chef)\b/i.test(desc)) return "Food & places";
123
+ if (/\b(community|volunteer|volunteers|library|libraries|neighborhood|nonprofit|charity|youth|senior|seniors)\b/i.test(desc)) return "Community";
124
+ }
125
+ const text = `${item.title} ${item.excerpt ?? ""}`;
126
+ if (/\b(health|hospitals?|clinics?|medical|doctors?|pharmac(?:y|ies)|wellness|veterinar(?:y|ian)|pet care|animal welfare|vaccinat(?:ion|ions)|patients?)\b/iu.test(text)) return "Health";
127
+ if (/\b(real estate|homeownership|mortgage|housing|tenants?|apartments?|below-market rate|home sales?|condos?)\b/iu.test(text)) return "Real Estate";
128
+ if (/\b(city council|planning commission|board of supervisors|ballot|measure [a-z0-9]|election|transit|traffic|police|firefighter|wildfire|evacuation|pedestrian safety|city manager|public works)\b/iu.test(text)) return "Civic life";
129
+ if (/\b(storefront|retail|downtown business|chamber of commerce|opens? doors?|boutique|merchants?|shoppers?|commercial space)\b/iu.test(text)) return "Local businesses";
130
+ if (/\b(festival|parade|fair|concert|workshop|exhibition|showcase|celebration|weigh-off|halloween|trunk-or-treat)\b/iu.test(text)) return "Events";
131
+ if (/\b(schools?|students?|teachers?|campuses?|classrooms?|high school|elementary|school district)\b/iu.test(text)) return "Schools";
132
+ if (/\b(restaurants?|cafes?|baker(?:y|ies)|winer(?:y|ies)|brewer(?:y|ies)|dining|chefs?|menus?)\b/iu.test(text)) return "Food & places";
133
+ return "Community";
134
+ }
93
135
  function orderedSections(sections2, order = EDITORIAL_SECTION_ORDER) {
94
136
  return sections2.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
95
137
  const leftRank = order.indexOf(left.name);
@@ -111,8 +153,8 @@ function sectionDefinition(name) {
111
153
  function editorialSections(items) {
112
154
  const groups = /* @__PURE__ */ new Map();
113
155
  for (const item of items) {
114
- const name = item.section?.trim() || "Local news";
115
- groups.set(name, [...groups.get(name) ?? [], item]);
156
+ const name = resolveSection(item);
157
+ groups.set(name, [...groups.get(name) ?? [], { ...item, section: name }]);
116
158
  }
117
159
  return Array.from(groups, ([name, articles]) => ({ ...sectionDefinition(name), items: articles }));
118
160
  }
@@ -3492,6 +3534,7 @@ exports.renderArticleBlocksToHtml = renderArticleBlocksToHtml;
3492
3534
  exports.resolveCoupon = resolveCoupon;
3493
3535
  exports.resolveEffectiveTheme = resolveEffectiveTheme;
3494
3536
  exports.resolveLayout = resolveLayout;
3537
+ exports.resolveSection = resolveSection;
3495
3538
  exports.revalidateDecision = revalidateDecision;
3496
3539
  exports.safeSponsorUrl = safeSponsorUrl;
3497
3540
  exports.sanitizeInlineMarkdown = sanitizeInlineMarkdown;
package/dist/index.d.cts CHANGED
@@ -221,6 +221,13 @@ declare function defaultRevalidateTargets(): {
221
221
  * "Opinion" are reserved so future sections slot in without a release.
222
222
  */
223
223
  declare const EDITORIAL_SECTION_ORDER: readonly ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health", "Schools", "Opinion"];
224
+ /**
225
+ * Resolves a story's canonical editorial section.
226
+ * Preserves explicitly assigned sections (normalizing known aliases).
227
+ * If unassigned, intelligently infers section desk from tags, categories, or keywords
228
+ * so stories are never lost to unrendered buckets.
229
+ */
230
+ declare function resolveSection(item: NewsItem): string;
224
231
  /** Sorts sections into the canonical order, dropping the implicit "Local news" bucket. */
225
232
  declare function orderedSections<T extends {
226
233
  name: string;
@@ -506,4 +513,4 @@ interface SponsorshipReport {
506
513
  /** Shared by the newsroom print/export view and the repeatable PDF renderer. */
507
514
  declare function campaignReportHtml(report: SponsorshipReport): string;
508
515
 
509
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, Measurement, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, type SponsorshipReport, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, clearTheme, composeEdition, composeLocalBriefing, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
516
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, Measurement, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, type SponsorshipReport, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, clearTheme, composeEdition, composeLocalBriefing, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/dist/index.d.ts CHANGED
@@ -221,6 +221,13 @@ declare function defaultRevalidateTargets(): {
221
221
  * "Opinion" are reserved so future sections slot in without a release.
222
222
  */
223
223
  declare const EDITORIAL_SECTION_ORDER: readonly ["Civic life", "Community", "Local businesses", "Real Estate", "Food & places", "Events", "Health", "Schools", "Opinion"];
224
+ /**
225
+ * Resolves a story's canonical editorial section.
226
+ * Preserves explicitly assigned sections (normalizing known aliases).
227
+ * If unassigned, intelligently infers section desk from tags, categories, or keywords
228
+ * so stories are never lost to unrendered buckets.
229
+ */
230
+ declare function resolveSection(item: NewsItem): string;
224
231
  /** Sorts sections into the canonical order, dropping the implicit "Local news" bucket. */
225
232
  declare function orderedSections<T extends {
226
233
  name: string;
@@ -506,4 +513,4 @@ interface SponsorshipReport {
506
513
  /** Shared by the newsroom print/export view and the repeatable PDF renderer. */
507
514
  declare function campaignReportHtml(report: SponsorshipReport): string;
508
515
 
509
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, Measurement, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, type SponsorshipReport, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, clearTheme, composeEdition, composeLocalBriefing, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
516
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, type Attribution, BROADSHEET_THEME, type BriefingCandidate, type BriefingPreferences, type BriefingReason, type BriefingSection, type BriefingSource, type BriefingVerification, COOKIE_CATEGORIES, CURATED_COMMUNITY_CARDS, CouponOffer, EDITORIAL_SECTION_ORDER, EVENTS, EditorialSection, type EffectiveTheme, FeatureName, HomepageSelection, LAYOUT_COOKIE, LOCAL_BRIEFING_POLICY_VERSION, type ListeningArticle, type LocalBriefing, type LocalBriefingItem, type LocalBriefingOptions, Measurement, NEWS_CACHE, NearbyUpdate, NewsArticle, NewsError, NewsItem, NewsPublication, PublicationInput, PublicationTheme, SESSION_IDLE_MS, type SponsorshipReport, THEME_COOKIE, THEME_EVENT, THEME_MAX_AGE_SECONDS, THEME_STORAGE_KEY, type ThemeMode, UTM_FIELDS, UUID, activeNearby, applyTheme, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, clearTheme, composeEdition, composeLocalBriefing, defaultRevalidateTargets, definePublication, displayHeadline, edgeCacheControl, editorialSections, featureEnabled, feedCacheKey, feedCacheOptions, filterNews, getStoredTheme, getSystemTheme, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, recordConsentToCompliance, referralCode, resolveCoupon, resolveEffectiveTheme, resolveLayout, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, setTheme, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, subscribeTheme, themeInitScript, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/dist/index.js CHANGED
@@ -32,6 +32,48 @@ var EDITORIAL_SECTION_ORDER = [
32
32
  "Schools",
33
33
  "Opinion"
34
34
  ];
35
+ var SECTION_ALIASES = {
36
+ "Business": "Local businesses",
37
+ "Government": "Civic life",
38
+ "Arts & Culture": "Community",
39
+ "Food & drink": "Food & places",
40
+ "Things to do": "Events",
41
+ "Sports": "Community",
42
+ "Education": "Schools",
43
+ "Public Safety": "Civic life",
44
+ "Transit": "Civic life",
45
+ "Environment": "Civic life",
46
+ "Recreation": "Community"
47
+ };
48
+ function resolveSection(item) {
49
+ const explicit = item.section?.trim();
50
+ if (explicit && explicit !== "Local news") {
51
+ return SECTION_ALIASES[explicit] ?? explicit;
52
+ }
53
+ const descriptors = [
54
+ ...(item.tags ?? []).map((t) => t.name.toLowerCase()),
55
+ ...(item.categories ?? []).map((c) => c.name.toLowerCase())
56
+ ];
57
+ for (const desc of descriptors) {
58
+ if (/\b(health|wellness|medical|hospital|clinic|doctor|pharmacy|veterinar|pet care|animal welfare|vaccin)\b/i.test(desc)) return "Health";
59
+ if (/\b(real estate|housing|home|homes|mortgage|tenant|tenants|apartment|apartments|property|bmr)\b/i.test(desc)) return "Real Estate";
60
+ if (/\b(civic|government|council|election|ballot|measure|transit|traffic|police|fire|firefighter|wildfire|safety|policy|supervisor)\b/i.test(desc)) return "Civic life";
61
+ if (/\b(business|retail|commerce|commercial|downtown|store|shop|merchants?|chamber of commerce)\b/i.test(desc)) return "Local businesses";
62
+ if (/\b(event|events|festival|festivals|fair|fairs|concert|concerts|music|workshop|workshops|things to do|exhibition|performance)\b/i.test(desc)) return "Events";
63
+ if (/\b(school|schools|education|student|students|teacher|teachers|campus|college|university)\b/i.test(desc)) return "Schools";
64
+ if (/\b(food|dining|restaurant|restaurants|wine|winery|brewery|bakery|cafe|coffee|cuisine|chef)\b/i.test(desc)) return "Food & places";
65
+ if (/\b(community|volunteer|volunteers|library|libraries|neighborhood|nonprofit|charity|youth|senior|seniors)\b/i.test(desc)) return "Community";
66
+ }
67
+ const text = `${item.title} ${item.excerpt ?? ""}`;
68
+ if (/\b(health|hospitals?|clinics?|medical|doctors?|pharmac(?:y|ies)|wellness|veterinar(?:y|ian)|pet care|animal welfare|vaccinat(?:ion|ions)|patients?)\b/iu.test(text)) return "Health";
69
+ if (/\b(real estate|homeownership|mortgage|housing|tenants?|apartments?|below-market rate|home sales?|condos?)\b/iu.test(text)) return "Real Estate";
70
+ if (/\b(city council|planning commission|board of supervisors|ballot|measure [a-z0-9]|election|transit|traffic|police|firefighter|wildfire|evacuation|pedestrian safety|city manager|public works)\b/iu.test(text)) return "Civic life";
71
+ if (/\b(storefront|retail|downtown business|chamber of commerce|opens? doors?|boutique|merchants?|shoppers?|commercial space)\b/iu.test(text)) return "Local businesses";
72
+ if (/\b(festival|parade|fair|concert|workshop|exhibition|showcase|celebration|weigh-off|halloween|trunk-or-treat)\b/iu.test(text)) return "Events";
73
+ if (/\b(schools?|students?|teachers?|campuses?|classrooms?|high school|elementary|school district)\b/iu.test(text)) return "Schools";
74
+ if (/\b(restaurants?|cafes?|baker(?:y|ies)|winer(?:y|ies)|brewer(?:y|ies)|dining|chefs?|menus?)\b/iu.test(text)) return "Food & places";
75
+ return "Community";
76
+ }
35
77
  function orderedSections(sections2, order = EDITORIAL_SECTION_ORDER) {
36
78
  return sections2.filter((section) => section.name !== "Local news").slice().sort((left, right) => {
37
79
  const leftRank = order.indexOf(left.name);
@@ -53,8 +95,8 @@ function sectionDefinition(name) {
53
95
  function editorialSections(items) {
54
96
  const groups = /* @__PURE__ */ new Map();
55
97
  for (const item of items) {
56
- const name = item.section?.trim() || "Local news";
57
- groups.set(name, [...groups.get(name) ?? [], item]);
98
+ const name = resolveSection(item);
99
+ groups.set(name, [...groups.get(name) ?? [], { ...item, section: name }]);
58
100
  }
59
101
  return Array.from(groups, ([name, articles]) => ({ ...sectionDefinition(name), items: articles }));
60
102
  }
@@ -794,4 +836,4 @@ function campaignReportHtml(report) {
794
836
  <footer><p>${escape(report.reporting_note)}</p><p>${escape(report.editorial_independence)}</p><p>Campaign reference: ${escape(report.campaign_id)}</p></footer></body></html>`;
795
837
  }
796
838
 
797
- export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, LOCAL_BRIEFING_POLICY_VERSION, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, composeEdition, composeLocalBriefing, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, referralCode, resolveCoupon, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
839
+ export { ANALYTICS_COOKIE, ANALYTICS_VERSION, ArticleProvenance, EDITORIAL_SECTION_ORDER, EVENTS, LOCAL_BRIEFING_POLICY_VERSION, NEWS_CACHE, NewsError, SESSION_IDLE_MS, UTM_FIELDS, UUID, activeNearby, articleCacheKey, articleCacheOptions, articleMeta, articleUrl, attribution, briefSummary, campaignReportHtml, composeEdition, composeLocalBriefing, defaultRevalidateTargets, displayHeadline, edgeCacheControl, editorialSections, feedCacheKey, feedCacheOptions, filterNews, inventoryCacheOptions, llmsText, matchesStoryTag, measurementConsent, nearbyUpdates, newsArticleJsonLd, normalizedText, orderedSections, publicHttps, publicUrl, publicationJsonLd, publicationListeningCandidates, publicationListeningTrack, publicationMeta, readerSources, referralCode, resolveCoupon, resolveSection, revalidateDecision, secretsMatch, sectionDefinition, sectionId, sectionPath, shortDeck, shortText, shouldServeLastKnown, storyDate, storyTowns, townSlug, validCalendarDate, walnutCreekTimes, withLastKnown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -473,3 +473,49 @@
473
473
  color: var(--pub-muted, var(--news-muted, #545c55));
474
474
  font-weight: 400;
475
475
  }
476
+
477
+ /*
478
+ * Broadsheet Editorial Section Layout & Balanced Breakouts (The Economist standard)
479
+ * Prevents empty columns and half-filled voids when an only-child or odd trailing section renders.
480
+ */
481
+ .publication [class*="sectionCollection"] {
482
+ display: grid;
483
+ gap: 8px 32px;
484
+ }
485
+
486
+ @media (min-width: 960px) {
487
+ .publication [class*="sectionCollection"] {
488
+ grid-template-columns: repeat(2, minmax(0, 1fr));
489
+ gap: 16px 36px;
490
+ row-gap: 24px;
491
+ }
492
+
493
+ .publication [class*="sectionCollection"] > [class*="section"]:only-child,
494
+ .publication [class*="sectionCollection"] > [class*="section"]:last-child:nth-child(odd) {
495
+ grid-column: 1 / -1;
496
+ display: block;
497
+ margin-bottom: 32px;
498
+ }
499
+
500
+ .publication [class*="sectionCollection"] > [class*="section"]:only-child [class*="sectionStories"],
501
+ .publication [class*="sectionCollection"] > [class*="section"]:last-child:nth-child(odd) [class*="sectionStories"] {
502
+ display: grid;
503
+ grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
504
+ gap: 36px;
505
+ align-items: start;
506
+ }
507
+
508
+ .publication [class*="sectionCollection"] > [class*="section"]:only-child [class*="sectionFeature"],
509
+ .publication [class*="sectionCollection"] > [class*="section"]:last-child:nth-child(odd) [class*="sectionFeature"] {
510
+ display: block;
511
+ }
512
+
513
+ .publication [class*="sectionCollection"] > [class*="section"]:only-child [class*="sectionSupporting"],
514
+ .publication [class*="sectionCollection"] > [class*="section"]:last-child:nth-child(odd) [class*="sectionSupporting"] {
515
+ display: grid;
516
+ grid-template-columns: minmax(0, 1fr);
517
+ gap: 20px;
518
+ align-content: start;
519
+ }
520
+ }
521
+