Files
homaroma/.agents/teamwork_preview_explorer_m1_1/handoff.md
T
2026-06-28 07:58:15 -04:00

28 KiB

Handoff Report: Editorial Carousel split_screen Layout Investigation

This handoff report summarizes findings, analysis, and implementation proposals for introducing the split_screen layout configuration to the EditorialCarousel page section.


1. Observation

We directly investigated five code locations:

1.1 Types Definition

  • File: packages/types/index.ts
  • Lines: 375-388
  • Current schema code:
export const EditorialCarouselSectionPayloadSchema = z.object({
  title: LocalizedStringSchema,
  backgroundColor: z.string().optional(),
  textColor: z.string().optional(),
  slides: z.array(z.object({
    category: LocalizedStringSchema,
    title: LocalizedStringSchema,
    description: LocalizedStringSchema,
    imageUrl: z.string().min(1),
    buttonText: LocalizedStringSchema.optional(),
    buttonLink: z.string().optional(),
  })).min(1)
});
export type EditorialCarouselSectionPayload = z.infer<typeof EditorialCarouselSectionPayloadSchema>;

1.2 MCP Server Tool

  • File: apps/mcp-server/section-toolkit.ts
  • Lines: 265-292
  • Current properties structure:
    name: "update_editorial_carousel",
    sectionType: "EDITORIAL_CAROUSEL",
    description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "description": ${localizedStringExample} }`,
    properties: {
      sectionId: { type: "number", description: "The numeric ID of the section to update" },
      title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` },
      description: { ...localizedStringSchema, description: `Localized section description, for example ${localizedStringExample}` },
      backgroundColor: { type: "string", description: "Plain string background color in hex or rgba" },
      textColor: { type: "string", description: "Plain string text color in hex or rgba" },
      slides: { ... }
    }
  • Execution (Lines 550-578): The tool retrieves the payload properties defined in the config, extracts them from incoming params, fetches the existing section from backend (GET /pages/sections/:id?hydrate=false), merges the two payloads (mergedPayload = { ...existingRes.data.payload, ...payload }), and calls PUT /pages/sections/:id.

1.3 Backend Section Validation

  • File: apps/backend/src/routes/pages.ts
  • Lines: 451-465 (under PUT /sections/:sectionId), 536-578 (inside validateSectionPayload)
  • Validation logic:
function validateSectionPayload(
  type: string,
  payload: any,
): { success: boolean; issues?: z.ZodIssue[]; data?: any } {
  // ...
  switch (type.toUpperCase()) {
    // ...
    case "EDITORIAL_CAROUSEL":
      schema = EditorialCarouselSectionPayloadSchema;
      break;
    // ...
  }
  const res = schema.safeParse(payload);
  if (!res.success) {
    return { success: false, issues: res.error.issues };
  }
  return { success: true, data: res.data };
}

Validation output is saved directly into the database: payload: finalPayload = validationResult.data.

1.4 Admin Panel Page Builder Form

  • File: apps/admin/src/components/page-builder/EditorialCarouselForm.tsx
  • Lines: 43-86
  • Current properties updating pattern: Uses setEditingSection by merging state:
            onChange={(e) => setEditingSection({
              ...editingSection,
              payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
            })}

1.5 Client Renderers (SPA and Website)

  • SPA Component: apps/spa/src/components/DynamicEditorialCarousel.tsx (Lines 77-145)
    • Uses Swiper with standard slides-per-view breakpoints (slidesPerView: 1, 2, or 3).
    • Uses theme token-mapped utility classes: bg-pop-bg (resolved via bgMap), text-pop-black (resolved via textMap).
  • Website Renderer: apps/website/src/components/dynamic/DynamicPageRenderer.tsx (Lines 415-481)
    • Uses a horizontal flex scroll container with scrollContainerRef.
    • Buttons call scrollBy(400) to scroll horizontally.
    • Render card style uses .ui-card with w-[85vw] md:w-[350px].

2. Logic Chain

  1. Schema Extension (Types): To allow the layout properties (layout and imagePosition) to be saved, we must add them to EditorialCarouselSectionPayloadSchema in packages/types/index.ts. They must be .optional() so that existing sections in the database remain valid.
  2. Backend Validation Validation & Tool Exposure: Since pages.ts validates payload records using safeParse against the types schema, extending the schema will automatically allow the new parameters to pass backend validation. In the MCP server, we must also update the properties structure of update_editorial_carousel in apps/mcp-server/section-toolkit.ts to expose layout and imagePosition to AI agents.
  3. Form Fields (Admin): To enable administrative control over these layout configurations, we can add two select fields (Layout and Image Position) to the styling settings section in EditorialCarouselForm.tsx using the same merging style as backgroundColor. To enhance UX, the Image Position field can be disabled unless layout === 'split_screen'.
  4. Renderer Behavior (SPA & Website):
    • For layout === 'split_screen', we render exactly 1 slide at a time.
    • For the SPA component, Swiper configuration must adapt (slidesPerView={1} and no desktop breakpoints).
    • For the Next.js website component, slide width must stretch to 100% (w-full), and scrollBy must scroll by the full width of the container scrollContainerRef.current.clientWidth.
    • On desktop, CSS Grid will split the slide 35% text / 65% image. CSS order property will arrange them so that imagePosition: 'right' renders text-left/image-right, and imagePosition: 'left' renders image-left/text-right.
    • On mobile, CSS order will force the image to stack on top and text on the bottom.

3. Caveats

  • Existing Data Migrations: Since no database migrations are needed (fields are optional and schema validates them safely as optional), existing databases do not need prisma migrations.
  • Swiper Navigation Controls: In split_screen layout, standard Swiper arrows overlap the image/text on the far edges. Swiper handles pagination bullet styling, which has been configured to match bg-pop-pink.

4. Conclusion

The layout configuration changes are backward-compatible and require updates to:

  1. packages/types/index.ts
  2. apps/mcp-server/section-toolkit.ts
  3. apps/admin/src/components/page-builder/EditorialCarouselForm.tsx
  4. apps/spa/src/components/DynamicEditorialCarousel.tsx
  5. apps/website/src/components/dynamic/DynamicPageRenderer.tsx

Proposed Code Diffs

1. packages/types/index.ts

 export const EditorialCarouselSectionPayloadSchema = z.object({
   title: LocalizedStringSchema,
   backgroundColor: z.string().optional(),
   textColor: z.string().optional(),
+  layout: z.enum(['standard', 'split_screen']).optional(),
+  imagePosition: z.enum(['left', 'right']).optional(),
   slides: z.array(z.object({
     category: LocalizedStringSchema,
     title: LocalizedStringSchema,

2. apps/mcp-server/section-toolkit.ts

   {
     name: "update_editorial_carousel",
     sectionType: "EDITORIAL_CAROUSEL",
-    description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "description": ${localizedStringExample} }`,
+    description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, layout, imagePosition, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "layout": "split_screen", "imagePosition": "right" }`,
     properties: {
       sectionId: { type: "number", description: "The numeric ID of the section to update" },
       title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` },
       description: { ...localizedStringSchema, description: `Localized section description, for example ${localizedStringExample}` },
       backgroundColor: { type: "string", description: "Plain string background color in hex or rgba" },
       textColor: { type: "string", description: "Plain string text color in hex or rgba" },
+      layout: { type: "string", enum: ["standard", "split_screen"], description: "Layout style of the carousel (standard or split_screen)" },
+      imagePosition: { type: "string", enum: ["left", "right"], description: "Position of the image relative to text when layout is split_screen (left or right)" },
       slides: {

3. apps/admin/src/components/page-builder/EditorialCarouselForm.tsx

       <div className="grid grid-cols-2 gap-4">
         <div className="space-y-2">
           <label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
           ...
         </div>
         <div className="space-y-2">
           <label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
           ...
         </div>
       </div>
 
+      <div className="grid grid-cols-2 gap-4">
+        <div className="space-y-2">
+          <label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Layout</label>
+          <select
+            value={editingSection.payload?.layout || 'standard'}
+            onChange={(e) => setEditingSection({
+              ...editingSection,
+              payload: { ...(editingSection.payload || {}), layout: e.target.value }
+            })}
+            className="admin-input w-full"
+          >
+            <option value="standard">Standard Carousel</option>
+            <option value="split_screen">Split Screen</option>
+          </select>
+        </div>
+        <div className="space-y-2">
+          <label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Image Position (Split Screen)</label>
+          <select
+            disabled={editingSection.payload?.layout !== 'split_screen'}
+            value={editingSection.payload?.imagePosition || 'right'}
+            onChange={(e) => setEditingSection({
+              ...editingSection,
+              payload: { ...(editingSection.payload || {}), imagePosition: e.target.value }
+            })}
+            className="admin-input w-full disabled:opacity-50"
+          >
+            <option value="right">Right (Text Left, Image Right)</option>
+            <option value="left">Left (Image Left, Text Right)</option>
+          </select>
+        </div>
+      </div>
+
       <div className="space-y-4">

4. apps/spa/src/components/DynamicEditorialCarousel.tsx

 export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCarouselProps) {
   const { i18n } = useTranslation();
   const lang = i18n.language;
+  const layout = payload.layout || 'standard';
+  const imagePosition = payload.imagePosition || 'right';
+  const isSplitScreen = layout === 'split_screen';
+
   const title = getLocalizedField(payload.title, lang);
   const slides = (payload.slides || []).map((slide) => ({
     ...slide,
@@ -86,13 +90,13 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
         <div className="relative select-none pb-12">
           <Swiper
             modules={[Autoplay, Pagination, Navigation]}
-            spaceBetween={30}
+            spaceBetween={isSplitScreen ? 0 : 30}
             slidesPerView={1}
-            breakpoints={{
+            breakpoints={isSplitScreen ? undefined : {
               640: { slidesPerView: 2 },
               1024: { slidesPerView: 3 }
-            }}
+            }}
             pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
             navigation={true}
             autoplay={{ delay: 5000, disableOnInteraction: false }}
             className="w-full"
           >
             {slides.map((slide, idx) => (
               <SwiperSlide key={idx} className="h-auto flex flex-col">
-                <div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-pop-pink/30">
-                  <div className="aspect-[4/3] overflow-hidden relative">
-                    <img 
-                      src={slide.imageUrl} 
-                      alt={slide.title} 
-                      className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
-                      onError={(e) => {
-                        (e.target as HTMLImageElement).style.display = 'none';
-                      }}
-                    />
-                  </div>
-
-                  <div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
-                    <div className="space-y-3">
-                      <span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
-                        {slide.category}
-                      </span>
-                      <h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
-                        {renderTitle(slide.title) || slide.title}
-                      </h3>
-                      <p className="line-clamp-3 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
-                        {slide.description}
-                      </p>
-                    </div>
-
-                    {slide.buttonText && slide.buttonLink && (
-                      <div className="pt-2">
-                        <Link
-                          to={slide.buttonLink}
-                          className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
-                        >
-                          {slide.buttonText}
-                          <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
-                        </Link>
-                      </div>
-                    )}
-                  </div>
-                </div>
+                {isSplitScreen ? (
+                  <div className={`grid grid-cols-1 md:grid-cols-${imagePosition === 'left' ? '[65%_35%]' : '[35%_65%]'} w-full items-stretch border border-white/10 rounded-xl overflow-hidden bg-night-950/20`}>
+                    {/* Text Column */}
+                    <div className={`p-8 md:p-12 lg:p-16 flex flex-col justify-center space-y-6 order-2 md:order-${imagePosition === 'left' ? '2' : '1'}`}>
+                      <span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
+                        {slide.category}
+                      </span>
+                      <h3 className="font-serif text-2xl md:text-3xl lg:text-4xl font-light tracking-wide text-pop-black">
+                        {renderTitle(slide.title) || slide.title}
+                      </h3>
+                      <p className="text-sm font-light leading-relaxed text-pop-black/70">
+                        {slide.description}
+                      </p>
+                      {slide.buttonText && slide.buttonLink && (
+                        <div className="pt-2">
+                          <Link
+                            to={slide.buttonLink}
+                            className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
+                          >
+                            {slide.buttonText}
+                            <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+                          </Link>
+                        </div>
+                      )}
+                    </div>
+                    {/* Image Column */}
+                    <div className={`relative aspect-[4/3] md:aspect-auto md:h-full min-h-[300px] overflow-hidden order-1 md:order-${imagePosition === 'left' ? '1' : '2'}`}>
+                      <img 
+                        src={slide.imageUrl} 
+                        alt={slide.title} 
+                        className="w-full h-full object-cover transition-transform duration-700 ease-out hover:scale-105"
+                        onError={(e) => {
+                          (e.target as HTMLImageElement).style.display = 'none';
+                        }}
+                      />
+                    </div>
+                  </div>
+                ) : (
+                  <div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-pop-pink/30">
+                    <div className="aspect-[4/3] overflow-hidden relative">
+                      <img 
+                        src={slide.imageUrl} 
+                        alt={slide.title} 
+                        className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
+                        onError={(e) => {
+                          (e.target as HTMLImageElement).style.display = 'none';
+                        }}
+                      />
+                    </div>
+                    <div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
+                      <div className="space-y-3">
+                        <span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
+                          {slide.category}
+                        </span>
+                        <h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
+                          {renderTitle(slide.title) || slide.title}
+                        </h3>
+                        <p className="line-clamp-3 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
+                          {slide.description}
+                        </p>
+                      </div>
+                      {slide.buttonText && slide.buttonLink && (
+                        <div className="pt-2">
+                          <Link
+                            to={slide.buttonLink}
+                            className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
+                          >
+                            {slide.buttonText}
+                            <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+                          </Link>
+                        </div>
+                      )}
+                    </div>
+                  </div>
+                )}
               </SwiperSlide>
             ))}

5. apps/website/src/components/dynamic/DynamicPageRenderer.tsx

 function EditorialCarouselSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
   const i18nLang = useLang()
   const resolvedLang = lang === 'all' ? i18nLang : lang
   const title = localizedFromRecord(payload, 'title', resolvedLang)
   const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : []
   const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
   const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
   const bgClass = backgroundColor ? `bg-pop-${backgroundColor}` : 'bg-surface-page'
   const textClass = textColor ? `text-pop-${textColor}` : 'text-text-main'
 
+  const layout = typeof payload.layout === 'string' ? payload.layout : 'standard'
+  const imagePosition = typeof payload.imagePosition === 'string' ? payload.imagePosition : 'right'
+  const isSplitScreen = layout === 'split_screen'
+
   const scrollContainerRef = useRef<HTMLDivElement>(null)
   const scrollBy = (offset: number) => {
     if (scrollContainerRef.current) {
-      scrollContainerRef.current.scrollBy({ left: offset, behavior: 'smooth' })
+      if (isSplitScreen) {
+        const width = scrollContainerRef.current.clientWidth
+        scrollContainerRef.current.scrollBy({ left: offset > 0 ? width : -width, behavior: 'smooth' })
+      } else {
+        scrollContainerRef.current.scrollBy({ left: offset, behavior: 'smooth' })
+      }
     }
   }
 
   if (!slides.length) return null
@@ -432,38 +441,755  
             {slides.map((slide, index) => {
               const slideTitle = localizedFromRecord(slide, 'title', resolvedLang)
               const description = localizedFromRecord(slide, 'description', resolvedLang)
               const category = localizedFromRecord(slide, 'category', resolvedLang)
               const buttonText = localizedFromRecord(slide, 'buttonText', resolvedLang)
               const buttonLink = typeof slide.buttonLink === 'string' ? slide.buttonLink : ''
               const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : ''
 
-              return (
-                <article key={index} className="ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start">
-                  {imageUrl ? (
-                    <div className="relative aspect-[4/3] overflow-hidden">
-                      <Image
-                        src={imageUrl}
-                        alt={slideTitle || 'Editorial slide'}
-                        fill
-                        unoptimized
-                        className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
-                      />
-                    </div>
-                  ) : null}
-                  <div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
-                    <div>
-                      {category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
-                      {slideTitle && <h3 className="text-xl font-bold text-text-main mt-6 group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
-                      {description && <p className="text-base text-muted-text mt-3 whitespace-pre-wrap">{description}</p>}
-                    </div>
-                    {buttonText && buttonLink && (
-                      <div className="pt-2">
-                        <Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
-                          {buttonText} <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
-                        </Link>
-                      </div>
-                    )}
-                  </div>
-                </article>
-              )
+              return isSplitScreen ? (
+                <article key={index} className={`w-full flex-none snap-start grid grid-cols-1 md:grid-cols-${imagePosition === 'left' ? '[65%_35%]' : '[35%_65%]'} items-stretch border border-border-muted rounded-xl overflow-hidden`}>
+                  {/* Text Column */}
+                  <div className={`p-8 md:p-12 lg:p-16 flex flex-col justify-center space-y-6 order-2 md:order-${imagePosition === 'left' ? '2' : '1'}`}>
+                    {category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
+                    {slideTitle && <h3 className="text-2xl md:text-3xl lg:text-4xl font-bold text-text-main group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
+                    {description && <p className="text-sm font-light leading-relaxed text-muted-text whitespace-pre-wrap">{description}</p>}
+                    {buttonText && buttonLink && (
+                      <div className="pt-2">
+                        <Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
+                          {buttonText} <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+                        </Link>
+                      </div>
+                    )}
+                  </div>
+                  {/* Image Column */}
+                  {imageUrl ? (
+                    <div className={`relative aspect-[4/3] md:aspect-auto md:h-full min-h-[300px] overflow-hidden order-1 md:order-${imagePosition === 'left' ? '1' : '2'}`}>
+                      <Image
+                        src={imageUrl}
+                        alt={slideTitle || 'Editorial slide'}
+                        fill
+                        unoptimized
+                        className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
+                      />
+                    </div>
+                  ) : null}
+                </article>
+              ) : (
+                <article key={index} className="ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start">
+                  {imageUrl ? (
+                    <div className="relative aspect-[4/3] overflow-hidden">
+                      <Image
+                        src={imageUrl}
+                        alt={slideTitle || 'Editorial slide'}
+                        fill
+                        unoptimized
+                        className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
+                      />
+                    </div>
+                  ) : null}
+                  <div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
+                    <div>
+                      {category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
+                      {slideTitle && <h3 className="text-xl font-bold text-text-main mt-6 group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
+                      {description && <p className="text-base text-muted-text mt-3 whitespace-pre-wrap">{description}</p>}
+                    </div>
+                    {buttonText && buttonLink && (
+                      <div className="pt-2">
+                        <Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
+                          {buttonText} <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+                        </Link>
+                      </div>
+                    )}
+                  </div>
+                </article>
+              )
             })}
           </div>

5. Verification Method

To verify these changes after implementation:

5.1 Compilation and Type Check

Run the build command for the monorepo from the root directory to ensure type safety:

npm run build

5.2 Dynamic Form Verification

  1. Access the Admin Panel and navigate to the Page Builder.
  2. Select or create an Editorial Carousel section.
  3. Verify that the Layout option displays Standard Carousel and Split Screen.
  4. Select Split Screen and confirm that Image Position is enabled (it should be disabled when Standard Carousel is chosen).
  5. Switch Image Position between Left and Right and save the page.

5.3 Frontend Verification (Desktop and Mobile)

  1. Verify the layout renders correctly on desktop:
    • For split_screen, check that exactly 1 slide occupies the width.
    • Text takes up 35% of the width, and the Image takes up 65%.
    • When imagePosition is right, text is on the left, image on the right.
    • When imagePosition is left, image is on the left, text on the right.
  2. Verify layout on mobile:
    • Viewport width < 768px should stack the image on top and the text on the bottom for both left and right positions.
  3. Test compatibility by validating that sections with standard layouts remain untouched (3 columns on desktop, 1.5/2 cards on tablet/mobile).