diff --git a/libs/blocks/brick/brick.css b/libs/blocks/brick/brick.css
new file mode 100644
index 0000000000..89459a2fb6
--- /dev/null
+++ b/libs/blocks/brick/brick.css
@@ -0,0 +1,245 @@
+.brick {
+ position: relative;
+ display: flex;
+ text-size-adjust: none;
+ min-height: 300px;
+}
+
+.brick,
+.brick.click a.foreground {
+ color: inherit;
+}
+
+.brick.light,
+.brick.light.click a.foreground {
+ color: var(--text-color);
+}
+
+.brick.dark,
+.dark.brick.click a.foreground {
+ color: var(--color-white);
+}
+
+.brick .background {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: 0;
+ overflow: hidden;
+}
+
+.brick .foreground {
+ position: relative;
+ display: flex;
+ flex-grow: 1;
+ padding: var(--spacing-m);
+}
+
+.brick.rounded-corners,
+.brick.rounded-corners .background,
+.brick.rounded-corners .foreground {
+ border-radius: var(--spacing-xs);
+}
+
+.brick.align-center .foreground,
+.brick.align-center .foreground .action-area,
+.brick.center .foreground,
+.brick.center .foreground .action-area {
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+}
+
+.brick.center .foreground {
+ align-items: flex-start;
+}
+
+.brick .background div,
+.brick .background p,
+.brick .background picture {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+.brick .background p,
+.brick .background picture {
+ display: block;
+}
+
+.brick .background img {
+ object-fit: contain;
+ object-position: bottom center;
+ width: 100%;
+ height: 100%;
+}
+
+.brick .mobile-only,
+.brick .tablet-only,
+.brick .desktop-only {
+ display: none;
+}
+
+.brick .foreground p {
+ padding: 0;
+ margin: 0;
+}
+
+.brick .foreground div > * {
+ margin-top: var(--spacing-xs);
+}
+
+.brick .foreground p:first-child,
+.brick .foreground p.icon-area,
+.brick .foreground p.icon-area + p {
+ margin-top: 0;
+}
+
+.brick .foreground p.icon-area {
+ display: inline-block;
+ margin-bottom: var(--spacing-s);
+}
+
+.brick .foreground p.action-area {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+ margin-top: var(--spacing-s);
+}
+
+.brick .icon-stack-area li picture {
+ display: flex;
+ margin: 0;
+ padding: 0;
+ flex-shrink: 0;
+}
+
+.brick .foreground .icon-area picture {
+ display: flex;
+}
+
+.brick .icon-stack-area li img {
+ width: var(--icon-size-s);
+ height: auto;
+}
+
+.brick .foreground .icon-area img {
+ height: var(--icon-size-l);
+ width: auto;
+}
+
+.brick.click > a {
+ text-decoration: none;
+}
+
+.brick .icon-stack-area {
+ display: flex;
+ flex-flow: row wrap;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.brick.center .icon-stack-area,
+.brick.align-center .icon-stack-area {
+ display: inline-flex;
+ width: auto;
+}
+
+.brick .icon-stack-area li,
+.brick .icon-stack-area li a {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ text-align: left;
+}
+
+.brick .foreground a:not([class]),
+.brick .foreground span.first-link {
+ font-weight: 700;
+}
+
+[dir="rtl"] .brick .icon-stack-area li,
+[dir="rtl"] .brick .icon-stack-area li a {
+ text-align: right;
+}
+
+.brick.click a.foreground .first-link:not([class*="button"]) {
+ color: var(--link-color);
+ text-decoration: none;
+}
+
+.brick.click:hover a.foreground .first-link:not([class*="button"]) {
+ text-decoration: underline;
+ color: var(--link-hover-color);
+}
+
+.static-links .brick.click a.foreground .first-link:not([class*="button"]),
+.static-links .brick.click a.foreground a:not([class*="button"]),
+.brick.static-links.click a.foreground .first-link:not([class*="button"]),
+.brick.static-links.click a.foreground a:not([class*="button"]) {
+ color: inherit;
+ text-decoration: underline;
+}
+
+.brick.click:hover .first-link.con-button.blue,
+.brick.click:active .first-link.con-button.blue {
+ background: var(--color-accent-hover);
+ border-color: var(--color-accent-hover);
+ color: var(--color-white);
+}
+
+.brick.click:hover .first-link.con-button,
+.brick.click:active .first-link.con-button,
+.brick.light.click:hover .first-link.con-button,
+.brick.light.click:active .first-link.con-button,
+.light .brick.click:hover .first-link.con-button,
+.light .brick.click:active .first-link.con-button {
+ background-color: var(--color-black);
+ border-color: var(--color-black);
+ color: var(--color-white);
+}
+
+.dark .brick.click:hover .first-link.con-button,
+.brick.dark.click:active .first-link.con-button {
+ background-color: var(--color-white);
+ color: var(--color-black);
+ text-decoration: none;
+}
+
+
+@media screen and (max-width: 600px) {
+ .brick .mobile-only {
+ display: block;
+ }
+}
+
+@media screen and (min-width: 600px) {
+ .brick {
+ min-height: 384px;
+ }
+
+ .brick .foreground {
+ padding: var(--spacing-l);
+ }
+}
+
+@media screen and (min-width: 600px) and (max-width: 1199px) {
+ .brick .tablet-only {
+ display: block;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .brick .desktop-only {
+ display: block;
+ }
+
+ .brick.large {
+ min-height: 500px;
+ }
+}
diff --git a/libs/blocks/brick/brick.js b/libs/blocks/brick/brick.js
new file mode 100644
index 0000000000..90799735e7
--- /dev/null
+++ b/libs/blocks/brick/brick.js
@@ -0,0 +1,96 @@
+import { decorateTextOverrides, decorateBlockText, decorateBlockBg, decorateIconStack, decorateButtons } from '../../utils/decorate.js';
+import { createTag } from '../../utils/utils.js';
+
+const blockTypeSizes = {
+ large: ['xxl', 'm', 'l'],
+ default: ['xl', 'm', 'l'],
+};
+const objFitOptions = ['fill', 'contain', 'cover', 'none', 'scale-down'];
+
+function getBlockSize(el) {
+ const sizes = Object.keys(blockTypeSizes);
+ const size = sizes.find((s) => el.classList.contains(`${s}`)) || 'default';
+ return blockTypeSizes[size];
+}
+
+function handleSupplementalText(foreground) {
+ if (!foreground.querySelector('.action-area')) return;
+ const nextP = foreground.querySelector('.action-area + p');
+ const lastP = foreground.querySelector('.action-area ~ p:last-child');
+ if (nextP) nextP.className = '';
+ if (lastP) lastP.className = 'supplemental-text';
+}
+
+function setObjectFitAndPos(text, pic, bgEl) {
+ const backgroundConfig = text.split(',').map((c) => c.toLowerCase().trim());
+ const fitOption = objFitOptions.filter((c) => backgroundConfig.includes(c));
+ const focusOption = backgroundConfig.filter((c) => !fitOption.includes(c));
+ if (fitOption) [pic.querySelector('img').style.objectFit] = fitOption;
+ bgEl.innerHTML = '';
+ bgEl.append(pic);
+ bgEl.append(document.createTextNode(focusOption.join(',')));
+}
+
+function handleObjectFit(bgRow) {
+ const bgConfig = bgRow.querySelectorAll('div');
+ [...bgConfig].forEach((r) => {
+ const pic = r.querySelector('picture');
+ if (!pic) return;
+ let text = '';
+ const pchild = [...r.querySelectorAll('p:not(:empty)')].filter((p) => p.innerHTML.trim() !== '');
+ if (pchild.length > 2) text = pchild[1]?.textContent.trim();
+ if (!text && r.textContent) text = r.textContent;
+ if (!text) return;
+ setObjectFitAndPos(text, pic, r);
+ });
+}
+
+function handleClickableBrick(el, foreground) {
+ if (!el.classList.contains('click')) return;
+ const links = foreground.querySelectorAll('a');
+ if (links.length !== 1) { el.classList.remove('click'); return; }
+ const a = links[0];
+ const linkDiv = createTag('span', { class: [...a.classList, 'first-link'].join(' ') }, a.innerHTML);
+ a.replaceWith(linkDiv, a);
+ a.className = 'foreground';
+ el.appendChild(a);
+ a.innerHTML = foreground.innerHTML;
+ foreground.remove();
+}
+
+function decorateSupplementalText(el) {
+ const supplementalEl = el.querySelector('.foreground p.supplemental-text');
+ if (!supplementalEl) return;
+ supplementalEl.className = 'body-xs supplemental-text';
+}
+
+function decorateBricks(el) {
+ if (!el.classList.contains('light')) el.classList.add('dark');
+ const elems = el.querySelectorAll(':scope > div');
+ if (elems.length > 1) {
+ handleObjectFit(elems[elems.length - 2]);
+ decorateBlockBg(el, elems[elems.length - 2], { useHandleFocalpoint: true });
+ }
+ if (elems.length > 2) {
+ el.querySelector('.background').style.background = elems[0].textContent;
+ elems[0].remove();
+ }
+ const foreground = elems[elems.length - 1];
+ foreground.classList.add('foreground');
+ const hasIconArea = foreground.querySelector('p')?.querySelector('img');
+ if (hasIconArea) foreground.querySelector('p').classList.add('icon-area');
+ const blockFormatting = getBlockSize(el);
+ decorateButtons(foreground, 'button-l');
+ decorateBlockText(foreground, blockFormatting);
+ decorateIconStack(el);
+ el.querySelector('.icon-stack-area')?.classList.add('body-xs');
+ handleSupplementalText(foreground);
+ handleClickableBrick(el, foreground);
+ return foreground;
+}
+
+export default async function init(el) {
+ decorateBricks(el);
+ decorateTextOverrides(el);
+ decorateSupplementalText(el);
+}
diff --git a/libs/blocks/section-metadata/section-metadata.css b/libs/blocks/section-metadata/section-metadata.css
index e63a464342..20cda08030 100644
--- a/libs/blocks/section-metadata/section-metadata.css
+++ b/libs/blocks/section-metadata/section-metadata.css
@@ -189,10 +189,32 @@ main > .section[class*='-up'] > .content {
margin: 0;
}
+.section.masonry-layout {
+ display: grid;
+ grid-template-columns: repeat(1, 1fr);
+ gap: var(--spacing-s);
+ padding-left: var(--grid-margins-width);
+ padding-right: var(--grid-margins-width);
+}
+
+.section.masonry-layout > div[class*='grid-'],
+.section.masonry-layout > div[class*='grid-'] > div.fragment,
+.section.masonry-layout > div[class*='grid-'] > div.fragment > div.section {
+ display: grid;
+}
+
@media screen and (min-width: 600px) and (max-width: 1200px) {
.section.five-up {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
+
+ .section.masonry-layout {
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ }
+
+ .section.masonry-layout .grid-full-width:first-child {
+ grid-column: 1 / -1;
+ }
}
@media screen and (min-width: 720px) {
@@ -269,4 +291,22 @@ main > .section[class*='-up'] > .content {
padding-left: var(--grid-margins-width-10);
padding-right: var(--grid-margins-width-10);
}
+
+ .section.masonry-layout {
+ grid-template-columns: repeat(12, 1fr);
+ }
+
+ .section.masonry-layout .grid-full-width {grid-column: span 12; }
+ .section.masonry-layout .grid-half-width {grid-column: span 6; }
+ .section.masonry-layout .grid-span-1 {grid-column: span 1; }
+ .section.masonry-layout .grid-span-2 {grid-column: span 2; }
+ .section.masonry-layout .grid-span-3 {grid-column: span 3; }
+ .section.masonry-layout .grid-span-4 {grid-column: span 4; }
+ .section.masonry-layout .grid-span-5 {grid-column: span 5; }
+ .section.masonry-layout .grid-span-6 {grid-column: span 6; }
+ .section.masonry-layout .grid-span-7 {grid-column: span 7; }
+ .section.masonry-layout .grid-span-8 {grid-column: span 8; }
+ .section.masonry-layout .grid-span-9 {grid-column: span 9; }
+ .section.masonry-layout .grid-span-10 {grid-column: span 10; }
+ .section.masonry-layout .grid-span-11 {grid-column: span 11; }
}
diff --git a/libs/blocks/section-metadata/section-metadata.js b/libs/blocks/section-metadata/section-metadata.js
index 52db480f58..7dcd143755 100644
--- a/libs/blocks/section-metadata/section-metadata.js
+++ b/libs/blocks/section-metadata/section-metadata.js
@@ -27,6 +27,17 @@ export async function handleStyle(text, section) {
section.classList.add(...styles);
}
+function handleMasonry(text, section) {
+ section.classList.add(...['masonry-layout', 'masonry-up']);
+ const divs = section.querySelectorAll(":scope > div:not([class*='metadata'])");
+ const spans = [];
+ text.split('\n').forEach((line) => spans.push(...line.trim().split(',')));
+ [...divs].forEach((div, i) => {
+ const spanWidth = spans[i] ? spans[i] : 'span 4';
+ div.classList.add(`grid-${spanWidth.trim().replace(' ', '-')}`);
+ });
+}
+
function handleLayout(text, section) {
if (!(text || section)) return;
const layoutClass = `grid-template-columns-${text.replaceAll(' | ', '-')}`;
@@ -49,4 +60,5 @@ export default async function init(el) {
if (metadata.style) await handleStyle(metadata.style.text, section);
if (metadata.background) handleBackground(metadata, section);
if (metadata.layout) handleLayout(metadata.layout.text, section);
+ if (metadata.masonry) handleMasonry(metadata.masonry.text, section);
}
diff --git a/libs/utils/utils.js b/libs/utils/utils.js
index d1e9ae405d..c70f5dd769 100644
--- a/libs/utils/utils.js
+++ b/libs/utils/utils.js
@@ -13,6 +13,7 @@ const MILO_BLOCKS = [
'article-header',
'aside',
'author-header',
+ 'brick',
'bulk-publish',
'caas',
'caas-config',
diff --git a/test/blocks/brick/brick.test.js b/test/blocks/brick/brick.test.js
new file mode 100644
index 0000000000..81282d6130
--- /dev/null
+++ b/test/blocks/brick/brick.test.js
@@ -0,0 +1,87 @@
+import { readFile } from '@web/test-runner-commands';
+import { expect } from '@esm-bundle/chai';
+import { getLocale, setConfig } from '../../../libs/utils/utils.js';
+
+document.body.innerHTML = await readFile({ path: './mocks/body.html' });
+const { default: init } = await import('../../../libs/blocks/brick/brick.js');
+const { default: getFragment } = await import('../../../libs/blocks/fragment/fragment.js');
+
+const locales = { '': { ietf: 'en-US', tk: 'hah7vzn.css' } };
+const config = {
+ imsClientId: 'milo',
+ codeRoot: '/libs',
+ contentRoot: `${window.location.origin}${getLocale(locales).prefix}`,
+ locales,
+};
+setConfig(config);
+
+describe('basic brick', () => {
+ const bricks = document.querySelectorAll('.brick');
+ bricks.forEach((brick) => {
+ init(brick);
+ });
+ const fullCopy = document.querySelector('#full-copy');
+
+ it('has a heading-xxl for large brick', () => {
+ const heading = document.body.querySelector('.large .heading-xxl');
+ expect(heading).to.exist;
+ });
+
+ it('has a body-m', () => {
+ const bodyCopy = document.body.querySelector('.brick .foreground .body-m');
+ expect(bodyCopy).to.exist;
+ });
+
+ it('has a detail-l', () => {
+ const bodyCopy = document.body.querySelector('.brick .foreground .detail-l');
+ expect(bodyCopy).to.exist;
+ });
+
+ it('has text overrides', () => {
+ const heading = document.body.querySelector('#textOverride .foreground .heading-l');
+ const bodyCopy = document.body.querySelector('#textOverride .foreground .body-l');
+ expect(heading).to.exist;
+ expect(bodyCopy).to.exist;
+ });
+
+ it('has clickable brick', () => {
+ const a = document.body.querySelector('.click a');
+ expect(a.classList.contains('foreground')).to.be.true;
+ });
+
+ it('supports gradient color', () => {
+ const bgColor = document.body.querySelector('.brick .background');
+ expect(bgColor.style.background).to.not.be.null;
+ });
+
+ it('has background image', () => {
+ const bgImage = document.body.querySelector('.brick .background');
+ expect(bgImage.querySelector('picture')).to.exist;
+ });
+
+ it('has icon stack', () => {
+ expect(fullCopy.querySelector('.icon-stack-area')).to.exist;
+ });
+
+ it('has supplemental text with small font', () => {
+ expect(fullCopy.querySelector('.supplemental-text.body-xs')).to.exist;
+ });
+
+ it('has supplemental text with small font in override', () => {
+ expect(bricks[0].querySelector('.supplemental-text.body-xs')).to.exist;
+ });
+
+ it('renders in fragments', async () => {
+ const fragmentLink = document.body.querySelector('.fragment-link');
+ await getFragment(fragmentLink);
+ const fragment = document.querySelector('.fragment');
+ expect(fragment).to.exist;
+ });
+
+ it('fragment labels grid in section', async () => {
+ const fragment = document.querySelector('.fragment');
+ init(fragment);
+ const fullGrids = fragment.closest('.section.masonry-layout').querySelectorAll('.large');
+ expect(fullGrids.length > 2).to.be.true;
+ });
+});
diff --git a/test/blocks/brick/mocks/body.html b/test/blocks/brick/mocks/body.html
new file mode 100644
index 0000000000..7820dbf2ef
--- /dev/null
+++ b/test/blocks/brick/mocks/body.html
@@ -0,0 +1,489 @@
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XXL 44/55 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more Learn
+ more
+
Supplemental text
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, center, contain
+
+
+
+
+
+
Bottom, center, contain
+
+
+
+
+
+
Bottom, center, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, center, contain
+
+
+
+
+
+
Bottom, center, contain
+
+
+
+
+
+
Bottom, center, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XXL 44/55 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
+
+
+
linear-gradient(to right, #d7d2cc 0%, #304352 100%)
+
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
+
Bottom, right, contain
+
+
+
+
+
Heading XL 36/45 Lorem ipsum.
+
Body M regular (18/27) Lorem ipsum dolor sit amet.
+
Learn more
+
+
+
+
Fragment
+
+
+
+
+
+
+
+
+
+
bottom, center, contain
+
+
+
+
+
+
bottom, center, contain
+
+
+
+
+
+
bottom, center, contain
+
+
+
+
+
+
+
+
THE CREATIVITY CONFERENCE
+
Adobe MAX. It’s live now.
+
Grab your front-row seat at the creative event of the year.
+
+ -
+ Adobe Photoshop
+
+ -
+ Adobe Photoshop
+
+ -
+ Adobe Photoshop
+
+
+
Illustrator Photoshop
+
Text Link
+
Supplemental text
+
+
+
+
+
diff --git a/test/blocks/brick/mocks/fragments/body.plain.html b/test/blocks/brick/mocks/fragments/body.plain.html
new file mode 100644
index 0000000000..f120fab783
--- /dev/null
+++ b/test/blocks/brick/mocks/fragments/body.plain.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
bottom, center, contain
+
+
+
+
+
+
bottom, center, contain
+
+
+
+
+
+
bottom, center, contain
+
+
+
+
+
+
+
+
THE CREATIVITY CONFERENCE
+
Adobe MAX. It’s live now.
+
Grab your front-row seat at the creative event of the year.
+
+ -
+ Adobe Photoshop
+
+ -
+ Adobe Photoshop
+
+ -
+ Adobe Photoshop
+
+
+
Illustrator Photoshop
+
Text Link
+
Supplemental text
+
+
+
+
diff --git a/test/blocks/section-metadata/mocks/body.html b/test/blocks/section-metadata/mocks/body.html
index 6df377b541..9903e8fe5e 100644
--- a/test/blocks/section-metadata/mocks/body.html
+++ b/test/blocks/section-metadata/mocks/body.html
@@ -82,6 +82,22 @@
+
+
Block 1
+
Block 2
+
Block 3
+
Block 4
+
+