commit 19ee98c68dbff8451aae175d811f8f3f469bb612 Author: Matt Batchelder Date: Fri Feb 20 21:28:00 2026 -0500 init diff --git a/pages/about.php b/pages/about.php new file mode 100644 index 0000000..02dfb47 --- /dev/null +++ b/pages/about.php @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/pages/contact.php b/pages/contact.php new file mode 100644 index 0000000..d4061ea --- /dev/null +++ b/pages/contact.php @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/pages/demo.php b/pages/demo.php new file mode 100644 index 0000000..1e09f5c --- /dev/null +++ b/pages/demo.php @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/pages/devices.php b/pages/devices.php new file mode 100644 index 0000000..45ef71e --- /dev/null +++ b/pages/devices.php @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/faq.php b/pages/faq.php new file mode 100644 index 0000000..3b009ee --- /dev/null +++ b/pages/faq.php @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/features.php b/pages/features.php new file mode 100644 index 0000000..fb03643 --- /dev/null +++ b/pages/features.php @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/home.php b/pages/home.php new file mode 100644 index 0000000..74d4373 --- /dev/null +++ b/pages/home.php @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/pricing.php b/pages/pricing.php new file mode 100644 index 0000000..f38ec1c --- /dev/null +++ b/pages/pricing.php @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/pages/resources.php b/pages/resources.php new file mode 100644 index 0000000..fbda07e --- /dev/null +++ b/pages/resources.php @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/pages/solutions.php b/pages/solutions.php new file mode 100644 index 0000000..5a0ce11 --- /dev/null +++ b/pages/solutions.php @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/theme/assets/css/main.css b/theme/assets/css/main.css new file mode 100644 index 0000000..a531ae1 --- /dev/null +++ b/theme/assets/css/main.css @@ -0,0 +1,2350 @@ +/* ============================================================= + OTS Theme β€” Main Stylesheet + ============================================================= */ + +/* ── 1. CSS Variables ───────────────────────────────────────── */ +:root, +[data-theme="light"] { + /* ── Light palette β€” values come from Site Editor (Appearance β†’ Editor β†’ Styles) + Falls back to the default when no override has been saved. ── */ + --color-primary: var(--wp--preset--color--primary, #D83302); + --color-primary-dk: var(--wp--preset--color--primary-dk, #B52B02); + --color-primary-lt: var(--wp--preset--color--primary-lt, #FEF0EB); + --color-primary-rgb: 216,51,2; + --color-accent: var(--wp--preset--color--accent, #00757c); + --color-accent-dk: var(--wp--preset--color--accent-dk, #005a60); + --color-accent-lt: var(--wp--preset--color--accent-lt, #E6F4F5); + --color-accent-rgb: 0,117,124; + --color-dark: var(--wp--preset--color--dark, #0D1321); + --color-dark-2: var(--wp--preset--color--dark-2, #1a2236); + --color-text: var(--wp--preset--color--text, #2d3748); + --color-text-muted: var(--wp--preset--color--text-muted, #718096); + --color-border: var(--wp--preset--color--border, #e2e8f0); + --color-bg: var(--wp--preset--color--bg, #ffffff); + --color-bg-alt: var(--wp--preset--color--bg-alt, #FFF8F5); + --color-bg-dark: var(--wp--preset--color--dark, #0D1321); + --color-heading: var(--wp--preset--color--dark, #0D1321); + --header-scrolled-bg: rgba(255,255,255,.97); + --header-scrolled-text: var(--wp--preset--color--text, #2d3748); + --card-bg: var(--wp--preset--color--bg, #ffffff); + --form-bg: var(--wp--preset--color--bg-alt, #FFF8F5); + --form-bg-focus: var(--wp--preset--color--bg, #ffffff); + + /* ── Typography ── */ + --font-sans: var(--wp--preset--font-family--sans, 'Inter', system-ui, -apple-system, sans-serif); + --font-heading: var(--wp--preset--font-family--heading, var(--font-sans)); + + /* ── Border radii β€” editable via Appearance β†’ Editor β†’ Styles β†’ Custom ── */ + --radius-sm: var(--wp--custom--radius--sm, 6px); + --radius-md: var(--wp--custom--radius--md, 12px); + --radius-lg: var(--wp--custom--radius--lg, 20px); + --radius-xl: var(--wp--custom--radius--xl, 32px); + + --shadow-sm: 0 1px 3px rgba(0,0,0,.08); + --shadow-md: 0 4px 16px rgba(0,0,0,.10); + --shadow-lg: 0 12px 40px rgba(0,0,0,.14); + + --transition: 0.2s ease; + + /* ── Layout ── */ + --container-max: var(--wp--custom--container--max, 1200px); + --container-pad: var(--wp--custom--container--pad, clamp(1rem, 5vw, 2rem)); +} + +/* ── Dark Mode β€” values come from Site Editor custom settings + (Appearance β†’ Editor β†’ Styles β†’ Custom β†’ Dark) ────────────── */ +[data-theme="dark"] { + --color-primary: var(--wp--custom--dark--primary, #FF6B3D); + --color-primary-dk: var(--wp--custom--dark--primary-dk, #D83302); + --color-primary-lt: var(--wp--custom--dark--primary-lt, rgba(216,51,2,.15)); + --color-primary-rgb: 255,107,61; + --color-accent: var(--wp--custom--dark--accent, #00757c); + --color-accent-dk: var(--wp--custom--dark--accent-dk, #005a60); + --color-accent-lt: var(--wp--custom--dark--accent-lt, rgba(0,117,124,.15)); + --color-accent-rgb: 0,117,124; + --color-dark: var(--wp--custom--dark--dark, #E2E8F0); + --color-dark-2: var(--wp--custom--dark--dark-2, #CBD5E0); + --color-text: var(--wp--custom--dark--text, #CBD5E0); + --color-text-muted: var(--wp--custom--dark--text-muted, #A0AEC0); + --color-border: var(--wp--custom--dark--border, #2D3748); + --color-bg: var(--wp--custom--dark--bg, #0F1724); + --color-bg-alt: var(--wp--custom--dark--bg-alt, #151F30); + --color-bg-dark: var(--wp--custom--dark--bg-dark, #0A0F1A); + --color-heading: var(--wp--custom--dark--heading, #F7FAFC); + --header-scrolled-bg: rgba(15,23,36,.97); + --header-scrolled-text: var(--wp--custom--dark--text, #CBD5E0); + --card-bg: var(--wp--custom--dark--card-bg, #151F30); + --form-bg: var(--wp--custom--dark--card-bg, #151F30); + --form-bg-focus: #1A2538; + + --shadow-sm: 0 1px 3px rgba(0,0,0,.25); + --shadow-md: 0 4px 16px rgba(0,0,0,.35); + --shadow-lg: 0 12px 40px rgba(0,0,0,.45); +} + +/* ── 2. Reset / Base ────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; font-size: 16px; } + +body { + font-family: var(--font-sans); + color: var(--color-text); + background: var(--color-bg); + line-height: 1.65; + -webkit-font-smoothing: antialiased; +} + +img, video { max-width: 100%; display: block; } +a { color: inherit; text-decoration: none; transition: color var(--transition); } +a:hover { color: var(--color-primary); } + +/* Smooth theme transition */ +body, +.site-header, +.feature-card, +.pricing-card, +.contact-form-wrap, +.platform-visual, +.form-group input, +.form-group textarea, +.form-group select { transition: background var(--transition), color var(--transition), border-color var(--transition), box-shadow var(--transition); } +ul { list-style: none; } +button { font-family: inherit; cursor: pointer; border: none; background: none; } + +/* ── 3. Layout Utilities ───────────────────────────────────── */ +.container { + width: 100%; + max-width: var(--container-max); + margin-inline: auto; + padding-inline: var(--container-pad); +} + +.section { + padding-block: clamp(4rem, 8vw, 7rem); +} + +.section-alt { background: var(--color-bg-alt); } +.section-dark { + background: var(--color-bg-dark); + color: #fff; +} + +.section-header { + text-align: center; + max-width: 680px; + margin-inline: auto; + margin-bottom: 3.5rem; +} +.section-header .section-label { + display: inline-block; + font-size: .75rem; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--color-primary); + margin-bottom: .75rem; +} +.section-dark .section-header .section-label { color: var(--color-accent); } + +.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem; } +.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; } +.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.5rem; } + +@media (max-width: 960px) { + .grid-3 { grid-template-columns: repeat(2, 1fr); } + .grid-4 { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 640px) { + .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; gap: 1.25rem; } +} + +/* ── 4. Typography ─────────────────────────────────────────── */ +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading, var(--font-sans)); + font-weight: 700; + line-height: 1.2; + color: var(--color-heading); +} +.section-dark h1, +.section-dark h2, +.section-dark h3 { color: #fff; } + +h1 { font-size: clamp(2rem, 5vw, 3.5rem); } +h2 { font-size: clamp(1.6rem, 3.5vw, 2.4rem); } +h3 { font-size: clamp(1.1rem, 2vw, 1.35rem); } +h4 { font-size: 1rem; } + +p { margin-bottom: 1rem; } +p:last-child { margin-bottom: 0; } + +.lead { + font-size: clamp(1rem, 2vw, 1.2rem); + color: var(--color-text-muted); + line-height: 1.75; +} +.section-dark .lead { color: rgba(255,255,255,.75); } + +/* ── 5. Buttons ────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + gap: .5rem; + padding: .8rem 1.75rem; + border-radius: var(--radius-sm); + font-size: .95rem; + font-weight: 600; + transition: all var(--transition); + white-space: nowrap; + cursor: pointer; +} + +.btn-primary { + background: var(--color-primary); + color: #fff; +} +.btn-primary:hover { + background: var(--color-primary-dk); + color: #fff; + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(var(--color-primary-rgb),.3); +} + +.btn-outline { + border: 2px solid var(--color-primary); + color: var(--color-primary); + background: transparent; +} +.btn-outline:hover { + background: var(--color-primary); + color: #fff; + transform: translateY(-1px); +} + +.btn-ghost { + border: 2px solid rgba(255,255,255,.35); + color: #fff; + background: transparent; +} +.btn-ghost:hover { + background: rgba(255,255,255,.1); + border-color: rgba(255,255,255,.6); + color: #fff; +} + +.btn-sm { padding: .55rem 1.2rem; font-size: .875rem; } +.btn-lg { padding: 1rem 2.25rem; font-size: 1.05rem; } + +.btn-group { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +/* ── 6. Header ─────────────────────────────────────────────── */ +.site-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + padding-block: 1rem; + background: transparent; + transition: background var(--transition), box-shadow var(--transition), padding var(--transition); +} +.site-header.scrolled { + background: var(--header-scrolled-bg); + box-shadow: var(--shadow-sm); + padding-block: .65rem; + backdrop-filter: blur(8px); +} + +.site-header.scrolled .logo-text { color: var(--color-heading); } +.site-header.scrolled .nav-menu a { color: var(--header-scrolled-text); } + +.header-inner { + display: flex; + align-items: center; + gap: 2rem; + height: 70px; +} + +/* Logo */ +.site-logo { + display: flex; + align-items: center; + flex-shrink: 0; + max-height: 60px; + overflow: hidden; +} +.site-logo a { display: flex; align-items: center; } +.custom-logo-link { display: flex; align-items: center; } +.site-logo img, +.custom-logo-link img, +.custom-logo, +.header-inner img { + max-height: 60px !important; + width: auto !important; + height: auto !important; + max-width: 220px !important; + display: block !important; + object-fit: contain; +} +.logo-text { + font-size: 1.35rem; + font-weight: 300; + color: #fff; + letter-spacing: -.01em; + transition: color var(--transition); +} +.logo-text strong { font-weight: 800; color: var(--color-accent); } + +/* When header is over light background (inner pages) */ +.page-header-light .site-header { background: var(--color-bg); box-shadow: var(--shadow-sm); } +.page-header-light .logo-text { color: var(--color-heading); } +.page-header-light .nav-menu a { color: var(--color-text); } + +/* Nav */ +.site-nav { margin-left: auto; } +.nav-menu { + display: flex; + align-items: center; + gap: 2rem; +} +.nav-menu a { + font-size: .9rem; + font-weight: 500; + color: rgba(255,255,255,.85); + position: relative; + padding-bottom: 2px; +} + +/* Light mode: nav text over hero needs to be dark */ +[data-theme="light"] .nav-menu a { color: var(--color-text); } +[data-theme="light"] .nav-menu a:hover, +[data-theme="light"] .nav-menu .current-menu-item > a { color: var(--color-primary); } +[data-theme="light"] .logo-text { color: var(--color-heading); } +[data-theme="light"] .nav-toggle span { background: var(--color-heading); } +.nav-menu a::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: var(--color-accent); + transition: width var(--transition); +} +.nav-menu a:hover, +.nav-menu .current-menu-item > a { color: #fff; } +.nav-menu a:hover::after, +.nav-menu .current-menu-item > a::after { width: 100%; } + +.site-header.scrolled .nav-menu a { color: var(--header-scrolled-text); } +.site-header.scrolled .nav-menu a:hover, +.site-header.scrolled .nav-menu .current-menu-item > a { color: var(--color-primary); } + +/* ── Dropdown sub-menu ─────────────────────────────────────── */ +.nav-menu > li { + position: relative; +} + +/* Invisible bridge fills the gap between the link and the dropdown panel, + so moving the cursor downward never breaks the :hover state. */ +.nav-menu > li.menu-item-has-children::after { + content: ''; + position: absolute; + top: 100%; + left: -1rem; + right: -1rem; + height: .85rem; /* must be >= the gap set on .sub-menu (top: calc(100% + .75rem)) */ +} + +/* Caret indicator on parent items */ +.nav-menu > li.menu-item-has-children > a { + padding-right: 1.2em; +} +.nav-menu > li.menu-item-has-children > a::before { + content: ''; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-55%) rotate(45deg); + width: 5px; + height: 5px; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transition: transform var(--transition); +} +.nav-menu > li.menu-item-has-children:hover > a::before { + transform: translateY(-30%) rotate(225deg); +} + +/* Dropdown panel */ +.nav-menu .sub-menu { + position: absolute; + top: calc(100% + .75rem); + left: 50%; + transform: translateX(-50%); + min-width: 200px; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + padding: .5rem 0; + list-style: none; + margin: 0; + z-index: 200; + /* Hidden by default */ + opacity: 0; + pointer-events: none; + visibility: hidden; + transform: translateX(-50%) translateY(-6px); + transition: opacity var(--transition), transform var(--transition), visibility var(--transition); +} + +/* Arrow notch above the dropdown */ +.nav-menu .sub-menu::before { + content: ''; + position: absolute; + top: -6px; + left: 50%; + transform: translateX(-50%) rotate(45deg); + width: 10px; + height: 10px; + background: var(--color-bg); + border-top: 1px solid var(--color-border); + border-left: 1px solid var(--color-border); +} + +/* Show on hover */ +.nav-menu > li.menu-item-has-children:hover > .sub-menu, +.nav-menu > li.menu-item-has-children:focus-within > .sub-menu { + opacity: 1; + pointer-events: auto; + visibility: visible; + transform: translateX(-50%) translateY(0); +} + +/* Sub-menu links */ +.nav-menu .sub-menu a { + display: block; + padding: .55rem 1.25rem; + font-size: .875rem; + font-weight: 500; + color: var(--color-text); + white-space: nowrap; +} +.nav-menu .sub-menu a::after { display: none; } +.nav-menu .sub-menu a:hover, +.nav-menu .sub-menu .current-menu-item > a { + color: var(--color-primary); + background: var(--color-primary-lt); +} + +/* Dark mode overrides */ +[data-theme="dark"] .nav-menu .sub-menu { + background: var(--card-bg); + border-color: var(--color-border); +} +[data-theme="dark"] .nav-menu .sub-menu::before { + background: var(--card-bg); +} +[data-theme="dark"] .nav-menu .sub-menu a:hover, +[data-theme="dark"] .nav-menu .sub-menu .current-menu-item > a { + background: var(--color-primary-lt); +} + +.header-cta { margin-left: 1rem; } + +/* Mobile toggle */ +.nav-toggle { + display: none; + flex-direction: column; + gap: 5px; + margin-left: auto; + padding: 4px; +} +.nav-toggle span { + display: block; + width: 24px; + height: 2px; + background: #fff; + border-radius: 2px; + transition: all var(--transition); +} +.site-header.scrolled .nav-toggle span { background: var(--color-heading); } +.nav-toggle.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); } +.nav-toggle.open span:nth-child(2) { opacity: 0; } +.nav-toggle.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } + +@media (max-width: 768px) { + .nav-toggle { display: flex; } + .site-nav, .header-cta { display: none; } + .site-nav.open { + display: block; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: var(--color-dark); + z-index: 99; + padding: 5rem 2rem 2rem; + overflow-y: auto; + } + .site-nav.open .nav-menu { + flex-direction: column; + align-items: flex-start; + gap: 1.5rem; + } + .site-nav.open .nav-menu > li { + width: 100%; + } + .site-nav.open .nav-menu a { + color: #fff; + font-size: 1.25rem; + } + + /* Mobile sub-menu */ + .site-nav.open .nav-menu > li.menu-item-has-children > a { + padding-right: 2rem; + } + .site-nav.open .nav-menu > li.menu-item-has-children > a::before { + right: .25rem; + border-color: rgba(255,255,255,.7); + } + .site-nav.open .nav-menu > li.menu-item-has-children.submenu-open > a::before { + transform: translateY(-30%) rotate(225deg); + } + .site-nav.open .nav-menu .sub-menu { + position: static; + transform: none; + opacity: 1; + visibility: visible; + pointer-events: auto; + background: rgba(255,255,255,.07); + border: none; + border-radius: var(--radius-sm); + box-shadow: none; + padding: .5rem 0 .5rem .75rem; + margin-top: .5rem; + display: none; + } + .site-nav.open .nav-menu .sub-menu::before { display: none; } + .site-nav.open .nav-menu > li.menu-item-has-children.submenu-open > .sub-menu { + display: block; + } + .site-nav.open .nav-menu .sub-menu a { + font-size: 1rem; + padding: .4rem .75rem; + color: rgba(255,255,255,.8); + } + .site-nav.open .nav-menu .sub-menu a:hover { + color: #fff; + background: transparent; + } +} + +/* ── 7. Hero ───────────────────────────────────────────────── */ + +/* Datacenter canvas background (used by page-hero) */ +@keyframes dc-breathe { + 0%, 100% { filter: blur(1.5px) brightness(0.82) saturate(1.1); } + 50% { filter: blur(2.5px) brightness(0.76) saturate(1.2); } +} + +.dc-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 0; + filter: blur(2px) brightness(0.80) saturate(1.15); + animation: dc-breathe 8s ease-in-out infinite; + pointer-events: none; + display: block; + will-change: transform; +} + +.hero-overlay { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; + background: + linear-gradient( + 110deg, + rgba(5, 8, 15, 0.94) 35%, + rgba(5, 8, 15, 0.60) 68%, + rgba(5, 8, 15, 0.25) 100% + ), + linear-gradient( + to bottom, + rgba(5, 8, 15, 0.55) 0%, + rgba(5, 8, 15, 0.00) 15%, + rgba(5, 8, 15, 0.00) 85%, + rgba(5, 8, 15, 0.55) 100% + ); +} + +.hero { + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + background: #080d18; + color: #fff; +} + +.hero-inner { + position: relative; + z-index: 2; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + align-items: center; + padding-block: 8rem 5rem; +} + +.hero-label { + display: inline-flex; + align-items: center; + gap: .5rem; + background: rgba(var(--color-primary-rgb),.12); + border: 1px solid rgba(var(--color-primary-rgb),.25); + color: var(--color-primary); + font-size: .75rem; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + padding: .4rem 1rem; + border-radius: 100px; + margin-bottom: 1.5rem; +} + +.hero-title { + font-size: clamp(2.2rem, 5vw, 3.8rem); + font-weight: 900; + line-height: 1.1; + color: #fff; + margin-bottom: 1.5rem; +} +.hero-title .highlight { + color: var(--color-primary); +} + +.hero-description { + font-size: 1.1rem; + color: rgba(255,255,255,.7); + max-width: 520px; + margin-bottom: 2.5rem; + line-height: 1.8; +} + +.hero-stats { + display: flex; + gap: 2.5rem; + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid rgba(255,255,255,.1); +} +.hero-stat-value { + font-size: 1.75rem; + font-weight: 800; + color: #fff; +} +.hero-stat-label { + font-size: .8rem; + color: rgba(255,255,255,.5); + margin-top: .2rem; +} + +/* Hero visual β€” device illustration */ +.hero-visual { + display: flex; + justify-content: center; + align-items: flex-end; + min-height: 0; + align-self: center; +} + +/* ── Device grid ───────────────────────────────────────────── */ +.hero-devices { + display: flex; + align-items: flex-end; + gap: 1.75rem; + padding-bottom: 1rem; +} + +/* ── Individual device wrapper ─────────────────────────────── */ +.hero-device { + display: flex; + flex-direction: column-reverse; + align-items: center; + gap: .75rem; + opacity: 0; + animation: hero-device-in 1.4s ease-out forwards; +} +.hero-device--laptop { animation-delay: .3s; } +.hero-device--cloud { animation-delay: .6s; } +.hero-device--desktop { animation-delay: .9s; } +.hero-device--phone { animation-delay: 1.2s; } + +@keyframes hero-device-in { + from { opacity: 0; transform: translateY(18px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Device frame ──────────────────────────────────────────── */ +.hero-device__frame { + display: flex; + flex-direction: column; + align-items: center; +} + +/* ── Screen (shared) ───────────────────────────────────────── */ +.hero-device__screen { + background: var(--hero-screen-bg, #0f1623); + border: 1.5px solid var(--hero-screen-border, rgba(255,255,255,.15)); + border-radius: 8px 8px 0 0; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05); +} +.hero-device__screen-content { + padding: .75rem; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: .4rem; +} + +/* ── Laptop specifics ──────────────────────────────────────── */ +.hero-device--laptop .hero-device__screen { + width: 140px; + height: 95px; +} +.hero-device__base { + width: 160px; + height: 6px; + background: var(--hero-frame-bg, #1a2236); + border-radius: 0 0 4px 4px; + border: 1.5px solid var(--hero-screen-border, rgba(255,255,255,.15)); + border-top: none; +} + +/* -- Laptop app bars -- */ +.hero-device__app-bars { + display: flex; + flex-direction: column; + gap: 4px; + width: 80%; +} +.hero-device__app-bars div { + height: 5px; + border-radius: 2px; + background: var(--hero-bar-bg, rgba(255,255,255,.12)); +} +.hero-device__app-bars div:nth-child(1) { width: 100%; } +.hero-device__app-bars div:nth-child(2) { width: 75%; } +.hero-device__app-bars div:nth-child(3) { width: 55%; } +.hero-device__app-bars div:nth-child(4) { width: 40%; } + +/* ── Cloud (365Care) specifics ─────────────────────────────── */ +.hero-device--cloud .hero-device__frame { + align-items: center; +} +.hero-device__cloud-icon { + position: relative; + width: 80px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + filter: drop-shadow(0 2px 12px rgba(0,117,124,.3)); +} +.hero-device__cloud-icon svg { + width: 100%; + height: 100%; +} +.hero-device__cloud-icon svg path { + stroke-width: 2.5; +} +.hero-device__cloud-label { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-sans); + font-size: 1.1rem; + font-weight: 900; + color: var(--color-primary); + letter-spacing: -.02em; + padding-top: 6px; +} + +/* ── Desktop specifics ─────────────────────────────────────── */ +.hero-device--desktop .hero-device__screen { + width: 170px; + height: 115px; + border: 1.5px solid var(--color-accent); + box-shadow: 0 4px 24px rgba(0,117,124,.2), 0 0 0 1px rgba(0,117,124,.1); +} +.hero-device__stand { + width: 10px; + height: 18px; + background: var(--hero-frame-bg, #1a2236); + border-left: 1px solid var(--hero-screen-border, rgba(255,255,255,.1)); + border-right: 1px solid var(--hero-screen-border, rgba(255,255,255,.1)); +} +.hero-device__stand-base { + width: 50px; + height: 5px; + background: var(--hero-frame-bg, #1a2236); + border-radius: 0 0 3px 3px; + border: 1px solid var(--hero-screen-border, rgba(255,255,255,.1)); + border-top: none; +} + +/* Dashboard contents */ +.hero-device__dash-row { + display: flex; + gap: 6px; + width: 100%; +} +.hero-device__dash-card { + flex: 1; + height: 28px; + border-radius: 4px; + background: var(--hero-bar-bg, rgba(255,255,255,.08)); + border: 1px solid var(--hero-card-inner-border, rgba(255,255,255,.04)); +} +.hero-device__dash-bar { + width: 100%; + height: 6px; + border-radius: 3px; + background: var(--color-accent); + opacity: .65; +} +.hero-device__dash-bar--short { + width: 60%; + background: var(--color-primary); + opacity: .5; +} + +/* ── Phone specifics ───────────────────────────────────────── */ +.hero-device--phone .hero-device__screen { + width: 72px; + height: 120px; + border-radius: 12px; + border: 1.5px solid var(--hero-screen-border, rgba(255,255,255,.2)); + box-shadow: 0 4px 20px rgba(0,0,0,.3); +} +.hero-device__notif { + display: flex; + flex-direction: column; + align-items: center; + gap: .25rem; +} +.hero-device__notif-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background: rgba(34,197,94,.2); + color: #22c55e; + font-size: .9rem; + font-weight: 700; + box-shadow: 0 0 12px rgba(34,197,94,.25); +} +.hero-device__notif-text { + font-size: .6rem; + font-weight: 600; + color: var(--hero-text-secondary, rgba(255,255,255,.5)); + text-transform: uppercase; + letter-spacing: .08em; +} + +/* ── Service list (stacks from bottom) ─────────────────────── */ +.hero-device__services { + list-style: none; + display: flex; + flex-direction: column-reverse; + gap: .35rem; + width: 100%; +} +.svc { + display: flex; + align-items: center; + gap: .4rem; + font-size: .65rem; + font-weight: 600; + color: var(--hero-text-secondary, rgba(255,255,255,.6)); + background: var(--hero-svc-bg, rgba(255,255,255,.04)); + border: 1px solid var(--hero-svc-border, rgba(255,255,255,.06)); + border-radius: 6px; + padding: .3rem .5rem; + white-space: nowrap; + opacity: 0; + animation: svc-stack-in .8s ease-out forwards; +} +.svc__dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: #22c55e; + flex-shrink: 0; +} + +/* Stagger service items β€” stack upward from device */ +.hero-device--laptop .svc--1 { animation-delay: 1.2s; } +.hero-device--laptop .svc--2 { animation-delay: 1.5s; } +.hero-device--laptop .svc--3 { animation-delay: 1.8s; } +.hero-device--cloud .svc--1 { animation-delay: 1.5s; } +.hero-device--cloud .svc--2 { animation-delay: 1.8s; } +.hero-device--cloud .svc--3 { animation-delay: 2.1s; } +.hero-device--desktop .svc--1 { animation-delay: 1.8s; } +.hero-device--desktop .svc--2 { animation-delay: 2.1s; } +.hero-device--desktop .svc--3 { animation-delay: 2.4s; } +.hero-device--phone .svc--1 { animation-delay: 2.1s; } +.hero-device--phone .svc--2 { animation-delay: 2.4s; } + +@keyframes svc-stack-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Light / Dark theme vars for hero ──────────────────────── */ +:root, [data-theme="light"] { + --hero-screen-bg: #f7fafc; + --hero-screen-border: #e2e8f0; + --hero-frame-bg: #edf2f7; + --hero-bar-bg: #e2e8f0; + --hero-card-inner-border: #cbd5e0; + --hero-text-secondary: rgba(45,55,72,.6); + --hero-svc-bg: rgba(0,0,0,.03); + --hero-svc-border: #e2e8f0; +} +[data-theme="dark"] { + --hero-screen-bg: #0f1623; + --hero-screen-border: rgba(255,255,255,.1); + --hero-frame-bg: #1a2236; + --hero-bar-bg: rgba(255,255,255,.08); + --hero-card-inner-border: rgba(255,255,255,.04); + --hero-text-secondary: rgba(255,255,255,.55); + --hero-svc-bg: rgba(255,255,255,.04); + --hero-svc-border: rgba(255,255,255,.06); +} + +/* Light-mode hero overrides */ +[data-theme="light"] .hero { + background: var(--color-bg); + color: var(--color-text); +} +[data-theme="light"] .hero-title { + color: var(--color-heading); +} +[data-theme="light"] .hero-description { + color: var(--color-text-muted); +} +[data-theme="light"] .hero-stats { + border-top-color: var(--color-border); +} +[data-theme="light"] .hero-stat-value { + color: var(--color-heading); +} +[data-theme="light"] .hero-stat-label { + color: var(--color-text-muted); +} +[data-theme="light"] .hero-label { + background: rgba(var(--color-primary-rgb),.08); + border-color: rgba(var(--color-primary-rgb),.2); +} +[data-theme="light"] .hero-device__app-bars div { + background: #cbd5e0; +} +[data-theme="light"] .hero-device__screen { + box-shadow: 0 4px 20px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05); +} +[data-theme="light"] .hero-device--desktop .hero-device__screen { + box-shadow: 0 4px 24px rgba(0,117,124,.12), 0 0 0 1px rgba(0,117,124,.08); +} +[data-theme="light"] .hero-device--phone .hero-device__screen { + box-shadow: 0 4px 20px rgba(0,0,0,.08); +} +[data-theme="light"] .hero-device__cloud-icon { + filter: drop-shadow(0 2px 12px rgba(0,117,124,.2)); +} +[data-theme="light"] .hero-device__cloud-icon svg path { + stroke: var(--color-accent); +} +[data-theme="light"] .hero-device__notif-icon { + background: rgba(22,163,74,.12); + color: #16a34a; + box-shadow: 0 0 10px rgba(22,163,74,.15); +} +[data-theme="light"] .hero-device__dash-bar { + opacity: .5; +} +[data-theme="light"] .hero-device__dash-bar--short { + opacity: .35; +} +[data-theme="light"] .svc__dot { + background: #16a34a; +} +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + .hero-device, + .svc { + animation: none; + opacity: 1; + } +} + +@media (max-width: 900px) { + .hero-inner { grid-template-columns: 1fr; text-align: center; } + .hero-description { margin-inline: auto; } + .btn-group { justify-content: center; } + .hero-stats { justify-content: center; } + .hero-visual { margin-top: 2rem; } + .hero-devices { gap: 1rem; justify-content: center; } + .hero-device--laptop .hero-device__screen { width: 110px; height: 75px; } + .hero-device__base { width: 126px; } + .hero-device__cloud-icon { width: 64px; height: 48px; } + .hero-device__cloud-label { font-size: .9rem; } + .hero-device--desktop .hero-device__screen { width: 130px; height: 88px; } + .hero-device--phone .hero-device__screen { width: 58px; height: 96px; } + .svc { font-size: .58rem; padding: .25rem .4rem; } +} +@media (max-width: 480px) { + .hero-devices { flex-wrap: wrap; gap: 1.5rem; } + .hero-device--desktop { order: -1; } +} + +/* ── 8. Shared Card Base ───────────────────────────────────── */ +.oribi-card { + background: var(--card-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 2rem; + transition: all var(--transition); + position: relative; +} +.oribi-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-4px); + border-color: var(--color-primary-lt); +} +.oribi-card h3 { margin-bottom: .5rem; font-size: 1.1rem; } +.oribi-card p { color: var(--color-text-muted); font-size: .95rem; } + +/* ── Card Image System ─────────────────────────────────────── */ +.oribi-card-img { + display: block; + max-width: 100%; + border-radius: var(--radius-sm); +} +.oribi-card-img--contain { object-fit: contain; } +.oribi-card-img--cover { object-fit: cover; } +.oribi-card-img-wrap { + line-height: 0; + overflow: hidden; +} + +/* Image position: top */ +.oribi-card.img-top .oribi-card-img-wrap { + margin-bottom: 1.25rem; +} + +/* Image position: left β€” side-by-side */ +.oribi-card.img-left { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 1.25rem; +} +.oribi-card.img-left .oribi-card-img-wrap { + flex-shrink: 0; +} +.oribi-card.img-left .oribi-card-body { + flex: 1; + min-width: 0; +} +@media (max-width: 480px) { + .oribi-card.img-left { + flex-direction: column; + } +} + +/* Image position: replace-icon β€” image takes icon slot */ +.oribi-card.img-replace-icon .oribi-card-img-wrap { + margin-bottom: 1.25rem; +} + +/* Image position: background */ +.oribi-card.img-bg { + overflow: hidden; + color: #fff; +} +.oribi-card.img-bg .oribi-card-img-wrap { + position: absolute; + inset: 0; + z-index: 0; +} +.oribi-card.img-bg .oribi-card-img-wrap .oribi-card-img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 0; +} +.oribi-card.img-bg::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(to bottom, rgba(13,19,33,.55), rgba(13,19,33,.85)); + z-index: 1; + pointer-events: none; +} +.oribi-card.img-bg .oribi-card-body { + position: relative; + z-index: 2; +} +.oribi-card.img-bg h3 { color: #fff; } +.oribi-card.img-bg p { color: rgba(255,255,255,.8); } + +/* ── 8a. Feature Card ──────────────────────────────────────── */ +.feature-card-link { + display: block; + text-decoration: none; + color: inherit; +} +.feature-card-link:hover { color: inherit; } +.feature-icon { + width: 52px; height: 52px; + background: linear-gradient(135deg, var(--color-primary-lt), rgba(var(--color-primary-rgb), .12)); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.25rem; + font-size: 1.4rem; + transition: transform var(--transition), background var(--transition); +} +/* Font Awesome inside icon containers β€” normalise line-height and prevent + the icon from inheriting an unexpected font-size from its wrapper. */ +.feature-icon i, .value-icon i { + line-height: 1; + font-size: inherit; +} +.oribi-card:hover .feature-icon { + transform: scale(1.1); + background: var(--color-primary); + color: #fff; +} + +/* ── 8b. Value Card ────────────────────────────────────────── */ +.value-card { + text-align: center; + border-top: 3px solid var(--color-accent); + border-radius: var(--radius-sm) var(--radius-sm) var(--radius-md) var(--radius-md); +} +.value-card:hover { + border-top-color: var(--color-accent-dk); +} +.value-icon { + width: 64px; height: 64px; + background: var(--color-accent-lt); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-inline: auto; + margin-bottom: 1.25rem; + font-size: 1.75rem; + transition: transform var(--transition), box-shadow var(--transition); +} +.value-card:hover .value-icon { + transform: scale(1.1) rotate(-8deg); + box-shadow: 0 4px 12px rgba(var(--color-accent-rgb), .25); +} + +/* ── 8c. Addon Card ────────────────────────────────────────── */ +.oribi-card.addon-card { + border-style: dashed; + border-left: 3px solid var(--color-accent); + border-left-style: solid; + background: linear-gradient(135deg, var(--card-bg) 0%, var(--color-accent-lt) 100%); +} +.oribi-card.addon-card:hover { + border-style: solid; + border-color: var(--color-accent); +} +.addon-tag { + display: inline-block; + font-size: var(--wp--preset--font-size--xs, .75rem); + background: var(--color-accent-lt); + color: var(--color-accent); + padding: .25rem .75rem; + border-radius: 999px; + margin-bottom: .75rem; + font-weight: 600; +} +.addon-card h3 { + color: var(--color-heading); +} +.addon-card p { + font-size: .9rem; +} + +/* ── 8d. Image Card ────────────────────────────────────────── */ +.image-card { + padding: 0; + overflow: hidden; + border-radius: var(--radius-lg); + border: none; + box-shadow: var(--shadow-sm); +} +.image-card:hover { + box-shadow: var(--shadow-lg); + border: none; +} +.image-card .oribi-card-body { padding: 1.5rem 2rem; } +.image-card.img-left .oribi-card-body { padding: 1.5rem 1.5rem 1.5rem 0; } +.image-card .oribi-card-img { + transition: transform .4s ease; +} +.image-card:hover .oribi-card-img { + transform: scale(1.05); +} + +/* ── 8e. Stat Card ─────────────────────────────────────────── */ +.stat-card { + text-align: center; + position: relative; + overflow: hidden; +} +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--color-primary), var(--color-accent)); +} +.stat-value { + font-size: var(--wp--preset--font-size--4-xl, 2.5rem); + font-weight: 800; + color: var(--color-primary); + line-height: 1; + margin-bottom: .25rem; + letter-spacing: -0.02em; +} +.stat-card:hover .stat-value { + background: linear-gradient(135deg, var(--color-primary), var(--color-accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} +.stat-label { + font-size: var(--wp--preset--font-size--md, 1rem); + font-weight: 600; + color: var(--color-heading); + text-transform: uppercase; + letter-spacing: .05em; + font-size: .85rem; +} +.stat-card p { + margin-top: .75rem; + padding-top: .75rem; + border-top: 1px solid var(--color-border); +} + +/* ── 8f. Link Card ─────────────────────────────────────────── */ +.link-card { + display: flex; + flex-direction: column; + border-left: 4px solid var(--color-dark-2); + border-radius: var(--radius-sm) var(--radius-md) var(--radius-md) var(--radius-sm); + background: var(--color-bg-alt); +} +.link-card:hover { + border-left-width: 5px; + border-left-color: var(--color-primary); + background: var(--color-primary-lt); +} +.link-card .feature-icon { + background: transparent; + padding: 0; + font-size: 1.6rem; + width: auto; + height: auto; + margin-bottom: 1rem; +} +.link-card:hover .feature-icon { + background: transparent; + transform: none; +} +.link-card-cta { + color: var(--color-primary); + font-weight: 600; + font-size: var(--wp--preset--font-size--sm, .875rem); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: .35rem; + margin-top: auto; + padding-top: .75rem; +} +.link-card-cta::after { + content: '\2192'; + transition: transform var(--transition); +} +.link-card:hover .link-card-cta::after { + transform: translateX(4px); +} +.link-card-cta:hover { text-decoration: underline; } + +/* ── 8g. Card Mobile Responsive ────────────────────────────── */ + +/* Tablet β€” tighten padding & typography */ +@media (max-width: 960px) { + .oribi-card { + padding: 1.5rem; + } + .stat-value { + font-size: var(--wp--preset--font-size--3-xl, 2rem); + } + .image-card .oribi-card-body { + padding: 1.25rem 1.5rem; + } +} + +/* Small screens β€” single-column adjustments */ +@media (max-width: 640px) { + .oribi-card { + padding: 1.25rem; + } + .oribi-card h3 { + font-size: 1rem; + } + .oribi-card p { + font-size: .9rem; + } + + /* Feature card: smaller icon */ + .feature-icon { + width: 44px; + height: 44px; + font-size: 1.2rem; + margin-bottom: 1rem; + } + + /* Value card: downsize icon, keep centered */ + .value-icon { + width: 52px; + height: 52px; + font-size: 1.5rem; + margin-bottom: 1rem; + } + + /* Addon card: softer gradient, thinner accent */ + .oribi-card.addon-card { + border-left-width: 2px; + } + + /* Image card: reduce body padding */ + .image-card .oribi-card-body { + padding: 1rem 1.25rem; + } + .image-card.img-left .oribi-card-body { + padding: 1rem 1.25rem; + } + .image-card { + border-radius: var(--radius-md); + } + + /* Stat card: scale down the value */ + .stat-value { + font-size: var(--wp--preset--font-size--2-xl, 1.75rem); + } + .stat-label { + font-size: .8rem; + } + .stat-card p { + font-size: .85rem; + } + + /* Link card: full-width friendly */ + .link-card { + border-left-width: 2px; + } + .link-card:hover { + border-left-width: 3px; + } + .link-card .feature-icon { + font-size: 1.4rem; + margin-bottom: .75rem; + } + + /* Image-left layout stacks at this breakpoint */ + .oribi-card.img-left { + flex-direction: column; + } + .image-card.img-left { + flex-direction: column; + } + .image-card.img-left .oribi-card-img-wrap { + width: 100%; + } +} + +/* Extra-small β€” compact for narrow phones */ +@media (max-width: 400px) { + .oribi-card { + padding: 1rem; + } + .feature-icon { + width: 40px; + height: 40px; + font-size: 1.1rem; + } + .value-icon { + width: 46px; + height: 46px; + font-size: 1.3rem; + } + .stat-value { + font-size: var(--wp--preset--font-size--xl, 1.5rem); + } + .image-card .oribi-card-body { + padding: .875rem 1rem; + } +} + +/* Touch devices β€” disable hover transform to prevent sticky states */ +@media (hover: none) { + .oribi-card:hover { + transform: none; + box-shadow: none; + border-color: var(--color-border); + } + .value-card:hover { + border-top-color: var(--color-accent); + } + .oribi-card.addon-card:hover { + border-style: dashed; + border-left-style: solid; + border-color: var(--color-border); + border-left-color: var(--color-accent); + } + .link-card:hover { + border-left-width: 4px; + border-left-color: var(--color-dark-2); + background: var(--color-bg-alt); + } + .image-card:hover { + box-shadow: var(--shadow-sm); + } + .image-card:hover .oribi-card-img { + transform: none; + } + .oribi-card:hover .feature-icon { + transform: none; + background: linear-gradient(135deg, var(--color-primary-lt), rgba(var(--color-primary-rgb), .12)); + color: inherit; + } + .value-card:hover .value-icon { + transform: none; + box-shadow: none; + } + .stat-card:hover .stat-value { + background: none; + -webkit-background-clip: unset; + -webkit-text-fill-color: var(--color-primary); + background-clip: unset; + } + .link-card:hover .link-card-cta::after { + transform: none; + } +} + +/* Legacy compat aliases (deprecated β€” use .oribi-card) */ +.feature-card { + background: var(--card-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 2rem; + transition: all var(--transition); +} +.feature-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-4px); + border-color: var(--color-primary-lt); +} +.card-image { + display: block; + height: auto; + max-width: 100%; + object-fit: contain; + border-radius: var(--radius-sm); +} +.card-image-wrap { line-height: 0; } +.card-img-left { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 1.25rem; +} +.card-img-left .card-image-wrap { flex-shrink: 0; } +.card-img-left-body { flex: 1; min-width: 0; } +.card-img-left h3 { margin-bottom: .5rem; font-size: 1.1rem; } +.card-img-left p { color: var(--color-text-muted); font-size: .95rem; margin: 0; } +@media (max-width: 480px) { + .card-img-left { flex-direction: column; } +} + +/* ── 9. Pricing ────────────────────────────────────────────── */ +.pricing-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 2rem; + max-width: 800px; + margin-inline: auto; +} +.pricing-grid-3 { + grid-template-columns: repeat(3, 1fr); + max-width: 1100px; +} +.pricing-card { + background: var(--card-bg); + border: 2px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 2.5rem; + position: relative; + transition: all var(--transition); +} +.pricing-card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-4px); +} +.pricing-card.featured { + border-color: var(--color-primary); + box-shadow: 0 0 0 4px rgba(var(--color-primary-rgb),.1); +} +.pricing-badge { + position: absolute; + top: -14px; + left: 50%; + transform: translateX(-50%); + background: var(--color-primary); + color: #fff; + font-size: .72rem; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + padding: .35rem 1rem; + border-radius: 100px; +} +.pricing-name { font-size: 1.25rem; font-weight: 700; margin-bottom: .5rem; } +.pricing-tagline { font-size: .9rem; color: var(--color-text-muted); margin-bottom: 1.5rem; } +.pricing-price { margin-bottom: 1.5rem; } +.pricing-amount { + font-size: 2.5rem; + font-weight: 900; + color: var(--color-heading); + line-height: 1; +} +.pricing-amount sup { font-size: 1.25rem; vertical-align: super; } +.pricing-per { font-size: .85rem; color: var(--color-text-muted); margin-top: .35rem; } +.pricing-features { + list-style: none; + margin-bottom: 2rem; +} +.pricing-features li { + display: flex; + align-items: flex-start; + gap: .6rem; + font-size: .9rem; + padding: .5rem 0; + border-bottom: 1px solid var(--color-border); + color: var(--color-text); +} +.pricing-features li:last-child { border-bottom: none; } +.pricing-check { color: var(--color-accent); font-size: 1rem; flex-shrink: 0; margin-top: 1px; } + +@media (max-width: 900px) { + .pricing-grid-3 { grid-template-columns: 1fr; max-width: 400px; } +} +@media (max-width: 640px) { + .pricing-grid { grid-template-columns: 1fr; } +} + +/* ── 10. Platform Feature Rows ─────────────────────────────── */ +.platform-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; + padding-block: 2rem; +} +.platform-row + .platform-row { + border-top: 1px solid var(--color-border); + margin-top: 2rem; +} +.platform-row.reverse .platform-text { order: 2; } +.platform-row.reverse .platform-visual { order: 1; } + +.platform-text h3 { font-size: 1.5rem; margin-bottom: 1rem; } +.platform-text p { color: var(--color-text-muted); } + +.platform-visual { + background: var(--color-bg-alt); + border-radius: var(--radius-md); + aspect-ratio: 4/3; + display: flex; + align-items: center; + justify-content: center; + font-size: 4rem; + border: 1px solid var(--color-border); +} +/* When an image is set, remove the framed background styling */ +.platform-visual.has-img { + background: none; + border: none; + aspect-ratio: unset; + font-size: inherit; +} + +@media (max-width: 768px) { + .platform-row { grid-template-columns: 1fr; } + .platform-row.reverse .platform-text, + .platform-row.reverse .platform-visual { order: unset; } +} + +/* ── 11. Page Hero (inner pages) ───────────────────────────── */ +.page-hero { + background: #080d18; + color: #fff; + padding: 8rem 0 5rem; + text-align: center; + position: relative; + overflow: hidden; +} +.page-hero .hero-overlay { + background: + radial-gradient( + ellipse at center, + rgba(5, 8, 15, 0.50) 0%, + rgba(5, 8, 15, 0.75) 60%, + rgba(5, 8, 15, 0.90) 100% + ), + linear-gradient( + to bottom, + rgba(5, 8, 15, 0.55) 0%, + rgba(5, 8, 15, 0.00) 20%, + rgba(5, 8, 15, 0.00) 80%, + rgba(5, 8, 15, 0.55) 100% + ); +} +.page-hero .container { position: relative; z-index: 2; } +.page-hero h1 { color: #fff; margin-bottom: 1rem; } +.page-hero .lead { color: rgba(255,255,255,.7); } +.page-hero .hero-label { margin-bottom: 1.5rem; } + +/* ── 11b. Animated Hero (OTS Signs) ────────────────────────── */ + +/* Particle keyframes */ +@keyframes particle-float { + 0% { transform: translateY(0) translateX(0) scale(1); opacity: 0; } + 10% { opacity: var(--p-opacity, 0.4); } + 90% { opacity: var(--p-opacity, 0.4); } + 100% { transform: translateY(var(--p-ty, -100vh)) translateX(var(--p-tx, 40px)) scale(var(--p-scale-end, 0.3)); opacity: 0; } +} + +@keyframes glow-drift { + 0% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; } + 50% { transform: translate(-40%, -60%) scale(1.2); opacity: 0.5; } + 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.3; } +} + +/* Particle container */ +.hero-particles { + position: absolute; + inset: 0; + overflow: hidden; + z-index: 1; + pointer-events: none; +} + +/* Individual particles β€” circles with staggered float animation */ +.hero-particle { + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + animation: particle-float var(--p-dur, 18s) var(--p-delay, 0s) linear infinite; + will-change: transform, opacity; +} + +/* 12 particles with unique positions, sizes, speeds, and trajectories */ +.hero-particle--1 { --p-dur: 22s; --p-delay: 0s; --p-tx: 60px; --p-ty: -110vh; --p-opacity: 0.35; --p-scale-end: 0.2; width: 8px; height: 8px; bottom: -10px; left: 8%; } +.hero-particle--2 { --p-dur: 28s; --p-delay: 3s; --p-tx: -40px; --p-ty: -105vh; --p-opacity: 0.25; --p-scale-end: 0.4; width: 14px; height: 14px; bottom: -20px; left: 20%; } +.hero-particle--3 { --p-dur: 20s; --p-delay: 1s; --p-tx: 80px; --p-ty: -115vh; --p-opacity: 0.3; --p-scale-end: 0.3; width: 6px; height: 6px; bottom: -8px; left: 35%; } +.hero-particle--4 { --p-dur: 26s; --p-delay: 5s; --p-tx: -60px; --p-ty: -100vh; --p-opacity: 0.4; --p-scale-end: 0.2; width: 10px; height: 10px; bottom: -12px; left: 50%; } +.hero-particle--5 { --p-dur: 24s; --p-delay: 2s; --p-tx: 30px; --p-ty: -108vh; --p-opacity: 0.2; --p-scale-end: 0.5; width: 20px; height: 20px; bottom: -25px; left: 65%; background: rgba(0,117,124,0.15); } +.hero-particle--6 { --p-dur: 30s; --p-delay: 7s; --p-tx: -20px; --p-ty: -112vh; --p-opacity: 0.35; --p-scale-end: 0.3; width: 12px; height: 12px; bottom: -15px; left: 78%; } +.hero-particle--7 { --p-dur: 19s; --p-delay: 4s; --p-tx: 50px; --p-ty: -106vh; --p-opacity: 0.2; --p-scale-end: 0.4; width: 16px; height: 16px; bottom: -20px; left: 90%; background: rgba(216,51,2,0.12); } +.hero-particle--8 { --p-dur: 25s; --p-delay: 6s; --p-tx: -70px; --p-ty: -102vh; --p-opacity: 0.3; --p-scale-end: 0.2; width: 7px; height: 7px; bottom: -10px; left: 15%; } +.hero-particle--9 { --p-dur: 32s; --p-delay: 8s; --p-tx: 45px; --p-ty: -110vh; --p-opacity: 0.15; --p-scale-end: 0.6; width: 24px; height: 24px; bottom: -30px; left: 42%; background: rgba(0,117,124,0.1); } +.hero-particle--10 { --p-dur: 21s; --p-delay: 10s; --p-tx: -35px; --p-ty: -108vh; --p-opacity: 0.3; --p-scale-end: 0.3; width: 9px; height: 9px; bottom: -12px; left: 58%; } +.hero-particle--11 { --p-dur: 27s; --p-delay: 12s; --p-tx: 25px; --p-ty: -104vh; --p-opacity: 0.2; --p-scale-end: 0.4; width: 18px; height: 18px; bottom: -22px; left: 72%; background: rgba(216,51,2,0.08); } +.hero-particle--12 { --p-dur: 23s; --p-delay: 9s; --p-tx: -55px; --p-ty: -106vh; --p-opacity: 0.25; --p-scale-end: 0.3; width: 11px; height: 11px; bottom: -14px; left: 5%; } + +/* Ambient glow effect behind content */ +.hero-animated__glow { + position: absolute; + top: 50%; + left: 50%; + width: 600px; + height: 600px; + border-radius: 50%; + background: radial-gradient(circle, rgba(0,117,124,0.15) 0%, rgba(0,117,124,0) 70%); + animation: glow-drift 12s ease-in-out infinite; + z-index: 1; + pointer-events: none; +} + +/* Animated hero layout β€” centered single-column content */ +.hero-animated__inner { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding-block: 10rem 6rem; +} + +.hero-animated__content { + max-width: 720px; +} + +.hero-animated__content .hero-title { + font-size: clamp(2.4rem, 5vw, 4rem); +} + +.hero-animated__content .hero-description { + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.hero-animated__content .btn-group { + justify-content: center; +} + +.hero-animated__content .hero-stats { + justify-content: center; +} + +.hero-stats--three { + display: flex; + gap: 3rem; + margin-top: 2.5rem; +} + +/* Page hero animated β€” inherits base page-hero + adds particles */ +.page-hero-animated { + position: relative; +} +.page-hero-animated .hero-particles { z-index: 0; } +.page-hero-animated .hero-animated__glow { z-index: 0; } +.page-hero-animated .hero-overlay { z-index: 1; } +.page-hero-animated .container { position: relative; z-index: 2; } + +/* Reduce motion for accessibility */ +@media (prefers-reduced-motion: reduce) { + .hero-particle, + .hero-animated__glow { + animation: none; + opacity: var(--p-opacity, 0.2); + } +} + +@media (max-width: 768px) { + .hero-animated__inner { padding-block: 7rem 4rem; } + .hero-stats--three { gap: 1.5rem; flex-wrap: wrap; justify-content: center; } +} + +/* ── 12. About page ────────────────────────────────────────── */ +.about-intro { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; +} +.about-intro-visual { + background: linear-gradient(135deg, #D83302 0%, #00757c 100%); + border-radius: var(--radius-xl); + aspect-ratio: 1; + max-width: 420px; + display: flex; + align-items: center; + justify-content: center; + font-size: 6rem; + color: #fff; + box-shadow: var(--shadow-lg); +} +/* When an image is set, remove the gradient box styling */ +.about-intro-visual.has-img { + background: none; + box-shadow: none; + aspect-ratio: unset; + font-size: inherit; + color: inherit; +} +@media (max-width: 768px) { + .about-intro { grid-template-columns: 1fr; } + .about-intro-visual { max-width: 280px; margin-inline: auto; } +} + +/* ── 13. Contact ────────────────────────────────────────────── */ +.contact-layout { + display: grid; + grid-template-columns: 1fr 1.6fr; + gap: 4rem; + align-items: start; +} +@media (max-width: 768px) { + .contact-layout { grid-template-columns: 1fr; } +} + +.contact-info h2 { margin-bottom: 1rem; } +.contact-info .lead { margin-bottom: 2rem; } + +.contact-method { + display: flex; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.5rem; +} +.contact-method-icon { + width: 44px; height: 44px; + background: var(--color-primary-lt); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + flex-shrink: 0; +} +.contact-method-label { font-size: .8rem; color: var(--color-text-muted); } +.contact-method-value { font-weight: 600; color: var(--color-heading); font-size: .95rem; } + +.contact-form-wrap { + background: var(--card-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 2.5rem; + box-shadow: var(--shadow-sm); +} +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } +@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } } + +.form-group { margin-bottom: 1.25rem; } +.form-group label { + display: block; + font-size: .85rem; + font-weight: 600; + color: var(--color-text); + margin-bottom: .4rem; +} +.form-group label .req { color: #D83302; } + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: .75rem 1rem; + font-family: var(--font-sans); + font-size: .95rem; + color: var(--color-text); + background: var(--form-bg); + border: 1.5px solid var(--color-border); + border-radius: var(--radius-sm); + transition: border-color var(--transition), box-shadow var(--transition); + outline: none; + resize: vertical; +} +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb),.12); + background: var(--form-bg-focus); +} +.form-group textarea { min-height: 140px; } + +.form-notice { + margin-top: 1rem; + font-size: .85rem; + padding: .75rem 1rem; + border-radius: var(--radius-sm); + display: none; +} +.form-notice.success { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; display: block; } +.form-notice.error { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; display: block; } + +/* ── 14. CTA Banner ─────────────────────────────────────────── */ +@keyframes cta-shimmer { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} +.cta-banner { + position: relative; + overflow: hidden; + background: linear-gradient(135deg, #D83302 0%, #E8450A 30%, #B52B02 60%, #D83302 100%); + background-size: 250% 250%; + animation: cta-shimmer 8s ease infinite; + color: #fff; + padding-block: 5rem; + text-align: center; +} +.cta-banner::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 80%, rgba(255,255,255,.12) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255,200,100,.10) 0%, transparent 50%); + pointer-events: none; +} +.cta-banner::after { + content: ''; + position: absolute; + inset: 0; + background-image: radial-gradient(rgba(255,255,255,.08) 1px, transparent 1px); + background-size: 24px 24px; + pointer-events: none; +} +.cta-banner .container { position: relative; z-index: 1; } +.cta-banner h2 { color: #fff; margin-bottom: 1rem; } +.cta-banner p { color: rgba(255,255,255,.85); margin-bottom: 2rem; font-size: 1.1rem; } + +/* ── 15. Footer ────────────────────────────────────────────── */ +.site-footer { + background: var(--color-dark); + color: rgba(255,255,255,.7); + padding-top: 4rem; +} +.footer-inner { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr; + gap: 2rem; + padding-bottom: 3rem; + border-bottom: 1px solid rgba(255,255,255,.08); +} +@media (max-width: 900px) { + .footer-inner { grid-template-columns: 1fr 1fr; } + .footer-brand { grid-column: 1 / -1; } +} +@media (max-width: 480px) { + .footer-inner { grid-template-columns: 1fr; } +} + +.footer-brand .logo-text { font-size: 1.5rem; color: #fff; } +.footer-tagline { font-size: .9rem; color: rgba(255,255,255,.5); margin-top: .75rem; margin-bottom: .5rem; } +.footer-location { font-size: .8rem; color: rgba(255,255,255,.4); } + +.footer-links { display: contents; } +.footer-col h4 { + font-size: .8rem; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + color: rgba(255,255,255,.4); + margin-bottom: 1rem; +} +.footer-col ul { display: flex; flex-direction: column; gap: .6rem; } +.footer-col a { + font-size: .875rem; + color: rgba(255,255,255,.6); + transition: color var(--transition); +} +.footer-col a:hover { color: #fff; } + +.footer-bottom { + padding-block: 1.5rem; +} +.footer-bottom p { + font-size: .8rem; + color: rgba(255,255,255,.3); + text-align: center; + margin: 0; +} + +/* ── 15a. FAQ Accordion ─────────────────────────────────────── */ +.faq-list { + max-width: 800px; + margin-inline: auto; +} +.faq-item { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + margin-bottom: 1rem; + background: var(--card-bg); + transition: box-shadow var(--transition), border-color var(--transition); +} +.faq-item[open] { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb),.08); +} +.faq-question { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1.25rem 1.5rem; + font-weight: 600; + font-size: 1rem; + color: var(--color-heading); + cursor: pointer; + list-style: none; + user-select: none; +} +.faq-question::-webkit-details-marker { display: none; } +.faq-question::marker { content: ''; } +.faq-toggle { + flex-shrink: 0; + width: 24px; + height: 24px; + position: relative; + transition: transform var(--transition); +} +.faq-toggle::before, +.faq-toggle::after { + content: ''; + position: absolute; + background: var(--color-text-muted); + border-radius: 2px; + transition: transform var(--transition); +} +.faq-toggle::before { + width: 14px; height: 2px; + top: 50%; left: 50%; + transform: translate(-50%, -50%); +} +.faq-toggle::after { + width: 2px; height: 14px; + top: 50%; left: 50%; + transform: translate(-50%, -50%); +} +.faq-item[open] .faq-toggle::after { + transform: translate(-50%, -50%) rotate(90deg); + opacity: 0; +} +.faq-answer { + padding: 0 1.5rem 1.25rem; + color: var(--color-text-muted); + font-size: .95rem; + line-height: 1.7; +} +.faq-answer p { margin-bottom: .5rem; } +.faq-answer p:last-child { margin-bottom: 0; } + +/* ── 15b. Comparison Table ──────────────────────────────────── */ +.comparison-table-wrap { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--card-bg); +} +.comparison-table { + width: 100%; + border-collapse: collapse; + font-size: .92rem; + min-width: 600px; +} +.comparison-table thead th { + background: var(--color-bg-alt); + font-weight: 700; + font-size: .85rem; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--color-heading); + padding: 1rem 1.25rem; + text-align: center; + border-bottom: 2px solid var(--color-border); + position: sticky; + top: 0; + z-index: 1; +} +.comparison-table thead th:first-child { + text-align: left; + min-width: 220px; +} +.comparison-table tbody td { + padding: .85rem 1.25rem; + border-bottom: 1px solid var(--color-border); + text-align: center; + color: var(--color-text); + vertical-align: middle; +} +.comparison-table tbody tr:last-child td { + border-bottom: none; +} +.comparison-table tbody td:first-child { + text-align: left; + font-weight: 500; +} +.comparison-feature-col { min-width: 220px; } +.comparison-feature-name { color: var(--color-text); } +.comparison-group-row td { + background: var(--color-bg-alt); + font-weight: 700; + font-size: .85rem; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--color-primary); + text-align: left !important; + padding: .75rem 1.25rem; +} +.comparison-yes { + color: var(--color-accent); + font-size: 1.1rem; + font-weight: 700; +} +.comparison-no { + color: var(--color-text-muted); + font-size: 1rem; +} +.comparison-table tbody tr:hover td { + background: rgba(var(--color-primary-rgb), .03); +} +@media (max-width: 640px) { + .comparison-table-wrap { margin-inline: calc(var(--container-pad) * -1); border-radius: 0; border-left: none; border-right: none; } +} + +/* ── 16. Misc ───────────────────────────────────────────────── */ +.text-center { text-align: center; } +.text-primary { color: var(--color-primary); } +.text-accent { color: var(--color-accent); } +.mt-1 { margin-top: .5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } +.mt-4 { margin-top: 2rem; } +.mb-4 { margin-bottom: 2rem; } + +/* Divider */ +.divider { + border: none; + border-top: 1px solid var(--color-border); + margin-block: 3rem; +} + +/* Screen reader only */ +.sr-only { + position: absolute; + width: 1px; height: 1px; + margin: -1px; padding: 0; + overflow: hidden; clip: rect(0,0,0,0); + white-space: nowrap; border: 0; +} + +/* Scroll to top */ +.scroll-top { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 44px; height: 44px; + background: var(--color-primary); + color: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + box-shadow: var(--shadow-md); + opacity: 0; + transform: translateY(10px); + transition: all var(--transition); + z-index: 50; + cursor: pointer; +} +.scroll-top.visible { + opacity: 1; + transform: translateY(0); +} + +/* ── 17. Theme Toggle ──────────────────────────────────────── */ +.theme-toggle { + position: relative; + width: 48px; + height: 26px; + background: var(--color-border); + border-radius: 100px; + border: none; + cursor: pointer; + margin-left: .75rem; + flex-shrink: 0; + transition: background var(--transition); +} +.theme-toggle:hover { background: var(--color-text-muted); } +.theme-toggle::before { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + background: #fff; + border-radius: 50%; + transition: transform var(--transition); + box-shadow: 0 1px 3px rgba(0,0,0,.2); +} +[data-theme="dark"] .theme-toggle { + background: var(--color-primary); +} +[data-theme="dark"] .theme-toggle::before { + transform: translateX(22px); +} +.theme-toggle-icon { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 14px; + height: 14px; + pointer-events: none; +} +.theme-toggle .sun-icon { + left: 5px; + color: #f59e0b; +} +.theme-toggle .moon-icon { + right: 5px; + color: #fff; + opacity: .7; +} + +/* ── Dark mode overrides ───────────────────────────────────── */ +[data-theme="dark"] .section-dark { background: #0A0F1A; } +[data-theme="dark"] .section-alt { background: var(--color-bg-alt); } + +[data-theme="dark"] .hero { background: #0A0F1A; } + +[data-theme="dark"] .page-hero { + background: #0A0F1A; +} + +[data-theme="dark"] .hero-animated { background: #060A12; } +[data-theme="dark"] .page-hero-animated { background: #060A12; } +[data-theme="dark"] .hero-animated__glow { + background: radial-gradient(circle, rgba(0,117,124,0.2) 0%, rgba(0,117,124,0) 70%); +} + +[data-theme="dark"] .site-footer { background: #080C15; } + +[data-theme="dark"] .site-nav.open { background: #0A0F1A; } + +[data-theme="dark"] .logo-text { color: #F7FAFC; } +[data-theme="dark"] .logo-text strong { color: var(--color-accent); } + +[data-theme="dark"] .nav-menu a { color: rgba(255,255,255,.8); } +[data-theme="dark"] .nav-menu a:hover, +[data-theme="dark"] .nav-menu .current-menu-item > a { color: var(--color-primary); } + +[data-theme="dark"] .nav-toggle span { background: #F7FAFC; } + +[data-theme="dark"] .cta-banner { + background: linear-gradient(135deg, #C02E02 0%, #D83302 30%, #9A2200 60%, #C02E02 100%); + background-size: 250% 250%; +} + +[data-theme="dark"] .btn-ghost { + border-color: rgba(255,255,255,.25); +} + +[data-theme="dark"] .platform-visual:not(.has-img) { + background: var(--color-bg-alt); + border-color: var(--color-border); +} +[data-theme="dark"] .platform-visual.has-img { + background: none; + border: none; +} + +[data-theme="dark"] .form-notice.success { + background: rgba(22,163,74,.1); + color: #4ade80; + border-color: rgba(22,163,74,.3); +} +[data-theme="dark"] .form-notice.error { + background: rgba(220,38,38,.1); + color: #f87171; + border-color: rgba(220,38,38,.3); +} + +[data-theme="dark"] .faq-item { background: var(--card-bg); border-color: var(--color-border); } +[data-theme="dark"] .faq-item[open] { border-color: var(--color-primary); } +[data-theme="dark"] .faq-question { color: var(--color-heading); } +[data-theme="dark"] .faq-toggle::before, +[data-theme="dark"] .faq-toggle::after { background: var(--color-text-muted); } + +[data-theme="dark"] .comparison-table-wrap { background: var(--card-bg); border-color: var(--color-border); } +[data-theme="dark"] .comparison-table thead th { background: var(--color-bg-alt); color: var(--color-heading); border-color: var(--color-border); } +[data-theme="dark"] .comparison-table tbody td { border-color: var(--color-border); color: var(--color-text); } +[data-theme="dark"] .comparison-group-row td { background: var(--color-bg-alt); color: var(--color-primary); } +[data-theme="dark"] .comparison-table tbody tr:hover td { background: rgba(var(--color-primary-rgb), .06); } + +@media (max-width: 768px) { + .theme-toggle { margin-left: 0; margin-right: .5rem; } +} + +/* ============================================================= + Block Editor Overrides + (loaded into the editor canvas via add_editor_style) + ============================================================= */ + +/* Remove Gutenberg's default group-block padding so our section + classes control all spacing */ +.wp-block-group { + padding: 0 !important; + margin: 0 !important; +} + +/* The editor root container should not add extra padding */ +.is-root-container { + padding: 0 !important; +} + +/* Make the editor canvas full-width so sections render at + their natural width instead of being constrained to post-content width */ +.editor-styles-wrapper .is-root-container > .wp-block { + max-width: 100% !important; + margin-left: 0 !important; + margin-right: 0 !important; +} +.editor-styles-wrapper .wp-block.wp-block-group { + max-width: 100% !important; +} + +/* Headings inside the editor should use our theme styles */ +.editor-styles-wrapper h1, +.editor-styles-wrapper h2, +.editor-styles-wrapper h3 { + font-family: var(--font-sans); +} + +/* Prevent Gutenberg from adding default margin on headings/paragraphs + since our sections control spacing */ +.editor-styles-wrapper .wp-block-heading, +.editor-styles-wrapper .wp-block-paragraph { + margin: 0; +} diff --git a/theme/assets/js/main.js b/theme/assets/js/main.js new file mode 100644 index 0000000..d5a26a9 --- /dev/null +++ b/theme/assets/js/main.js @@ -0,0 +1,612 @@ +/** + * OTS Theme β€” Main JS + */ + +/* ── Theme toggle (runs before DOMContentLoaded to prevent flash) ── */ +(function() { + const saved = localStorage.getItem('oribi-theme'); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const theme = saved || (prefersDark ? 'dark' : 'light'); + document.documentElement.setAttribute('data-theme', theme); +})(); + +document.addEventListener('DOMContentLoaded', () => { + /* ── Sticky header ──────────────────────────────────────── */ + const header = document.getElementById('site-header'); + if (header) { + window.addEventListener('scroll', () => { + header.classList.toggle('scrolled', window.scrollY > 40); + }, { passive: true }); + } + + /* ── Mobile nav toggle ──────────────────────────────────── */ + const toggle = document.getElementById('nav-toggle'); + const nav = document.getElementById('site-nav'); + if (toggle && nav) { + toggle.addEventListener('click', () => { + toggle.classList.toggle('open'); + nav.classList.toggle('open'); + const expanded = toggle.getAttribute('aria-expanded') === 'true'; + toggle.setAttribute('aria-expanded', !expanded); + }); + } + + /* ── Mobile sub-menu accordion ──────────────────────────── */ + if (nav) { + nav.addEventListener('click', (e) => { + // Only act in mobile (nav-toggle visible = mobile view) + if (!toggle || getComputedStyle(toggle).display === 'none') return; + + const parentLi = e.target.closest('.menu-item-has-children'); + if (!parentLi) return; + + // If the click is on the parent link itself (not a child link), toggle + const clickedLink = e.target.closest('a'); + const subMenu = parentLi.querySelector(':scope > .sub-menu'); + if (!subMenu) return; + + // If they clicked a sub-menu link, let it navigate normally + if (clickedLink && subMenu.contains(clickedLink)) return; + + // Toggle this item; collapse siblings + const isOpen = parentLi.classList.contains('submenu-open'); + parentLi.closest('.nav-menu') + .querySelectorAll('.menu-item-has-children.submenu-open') + .forEach(li => li.classList.remove('submenu-open')); + + if (!isOpen) { + parentLi.classList.add('submenu-open'); + // Prevent the parent anchor from navigating when toggling + if (clickedLink && parentLi.contains(clickedLink) && !subMenu.contains(clickedLink)) { + e.preventDefault(); + } + } + }); + } + + /* ── Scroll-to-top ──────────────────────────────────────── */ + const scrollBtn = document.getElementById('scroll-top'); + if (scrollBtn) { + window.addEventListener('scroll', () => { + scrollBtn.classList.toggle('visible', window.scrollY > 600); + }, { passive: true }); + scrollBtn.addEventListener('click', () => { + window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + } + + /* ── Light / Dark theme toggle ─────────────────────────── */ + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', () => { + const current = document.documentElement.getAttribute('data-theme') || 'light'; + const next = current === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('oribi-theme', next); + themeToggle.setAttribute('aria-label', + next === 'dark' ? 'Switch to light mode' : 'Switch to dark mode' + ); + }); + } + + /* ── Contact form (AJAX) ────────────────────────────────── */ + const form = document.getElementById('contact-form'); + const notice = document.getElementById('form-notice'); + if (form && typeof oribiAjax !== 'undefined') { + form.addEventListener('submit', async (e) => { + e.preventDefault(); + notice.className = 'form-notice'; + notice.style.display = 'none'; + + const data = new FormData(form); + data.append('action', 'oribi_contact'); + data.append('nonce', oribiAjax.nonce); + + try { + const res = await fetch(oribiAjax.url, { method: 'POST', body: data }); + const json = await res.json(); + notice.textContent = json.data; + notice.className = 'form-notice ' + (json.success ? 'success' : 'error'); + notice.style.display = 'block'; + if (json.success) form.reset(); + } catch { + notice.textContent = 'Something went wrong. Please try again.'; + notice.className = 'form-notice error'; + notice.style.display = 'block'; + } + }); + } + + /* ── Animate cards on scroll ────────────────────────────── */ + const cards = document.querySelectorAll('.oribi-card, .feature-card, .industry-card, .pricing-card, .value-card, .platform-row'); + if (cards.length && 'IntersectionObserver' in window) { + cards.forEach(c => { c.style.opacity = '0'; c.style.transform = 'translateY(24px)'; c.style.transition = 'opacity .5s ease, transform .5s ease'; }); + const io = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + io.unobserve(entry.target); + } + }); + }, { threshold: 0.1 }); + cards.forEach(c => io.observe(c)); + } +}); + + +/* -- Datacenter hero background canvas ----------------------------------------- */ +(function () { + const canvas = document.getElementById('dc-canvas'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + + /* + * Performance: + * 1. bgCanvas -- static geometry painted once per resize, blit each frame. + * 2. LED draws batched by colour -- one shadowBlur setup per colour (~4/frame). + * 3. 30 fps cap. + * 4. Visibility API + IntersectionObserver pause rAF when hidden/off-screen. + */ + + /* -- colour palette -------------------------------------------------------- */ + const ROOM_TOP = '#020509'; + const ROOM_BOT = '#030b08'; + const RACK_SHELL = '#111b2e'; /* outer frame -- dark navy */ + const RACK_SHELL2 = '#0c1422'; /* outer frame shadow side */ + const RACK_FACE = '#0d1728'; /* inner face panel background */ + const RAIL_FACE = '#141f35'; /* mounting rail column face */ + const RAIL_EDGE = '#1c2d4a'; /* rail inner-edge highlight */ + const SCREW_COL = '#0a1220'; /* screw/nut recesses on rails */ + const SRV_FACE = '#1a2840'; /* server 1U face -- lighter than rack */ + const SRV_STRIPE = '#1f3050'; /* top-edge highlight stripe */ + const SRV_SHADOW = '#0e1a2b'; /* bottom-edge shadow line */ + const SRV_OFF = '#0d1624'; /* unpowered slot */ + const PWRBTN_RING = '#243654'; /* power button outer ring */ + const PWRBTN_FACE = '#101e30'; /* power button recessed face */ + const VENT_SLOT = '#0b1421'; /* vent/louver slots */ + const BAY_SLOT = '#09111e'; /* drive bay recesses */ + const BAY_EDGE = '#1d2f48'; /* drive bay raised edge */ + const PATCH_BODY = '#111d30'; /* patch unit body */ + const PATCH_PORT = '#070d18'; /* patch port holes */ + const PATCH_LBL = '#0b1422'; /* patch label strip */ + const CAB_TROUGH = '#080f1c'; /* cable management trough */ + const LED_OFF = '#182438'; /* unlit LED placeholder */ + const LED_COLORS = { green: '#00f07a', amber: '#ffb200', red: '#ff3838', blue: '#00aaff' }; + + /* -- depth layers (back -> front) + railW = thickness of side mounting-rail columns + padTop = vertical padding inside rack before first unit -- */ + const LAYERS = [ + { alpha: 0.28, yShift: 0.12, rackW: 54, rackGap: 18, unitH: 12, unitGap: 1, railW: 5, padTop: 7, ledSz: 2, ledCols: 3, ledRows: 2 }, + { alpha: 0.55, yShift: 0.05, rackW: 80, rackGap: 28, unitH: 18, unitGap: 2, railW: 7, padTop: 9, ledSz: 3, ledCols: 3, ledRows: 2 }, + { alpha: 1.00, yShift: 0.00, rackW: 112, rackGap: 40, unitH: 25, unitGap: 2, railW: 10, padTop: 11, ledSz: 4, ledCols: 4, ledRows: 2 }, + ]; + + /* -- shared LED-position helpers (same formula in paintBg & drawLEDs) ----- */ + function ledOriginX(ux, uw, ledCols, ledSz) { + return ux + uw - ledCols * (ledSz + 3) - 3; + } + function ledOriginY(uy, unitH, ledRows, ledSz) { + return uy + ((unitH - ledRows * (ledSz + 2) + 1) / 2 | 0); + } + + /* -- state ----------------------------------------------------------------- */ + let layers = []; + const bgCanvas = document.createElement('canvas'); + const bgCtx = bgCanvas.getContext('2d'); + let scanY = 0; + let lastTs = 0; + let rafId; + let W = 1, H = 1; + + const FRAME_MS = 1000 / 30; /* 30 fps throttle */ + let fpsDebt = 0; + + /* -- build rack data for one layer ---------------------------------------- */ + function buildLayer(def) { + const { rackW, rackGap, unitH, unitGap, railW, padTop, ledCols, ledRows, yShift } = def; + const numRacks = Math.ceil(W / (rackW + rackGap)) + 2; + const racks = []; + + for (let r = 0; r < numRacks; r++) { + const rx = r * (rackW + rackGap) - rackGap; + const numUnits = Math.floor((H * (1 - Math.abs(yShift) * 2) - padTop * 2) / (unitH + unitGap)); + const units = []; + const activity = { + timer: Math.random() * 8000, period: 5000 + Math.random() * 12000, + active: false, burstTimer: 0, burstLength: 0, + }; + + for (let u = 0; u < numUnits; u++) { + const leds = []; + for (let l = 0; l < ledCols * ledRows; l++) { + const rnd = Math.random(); + const type = rnd < 0.68 ? 'green' : rnd < 0.86 ? 'amber' : rnd < 0.96 ? 'red' : 'blue'; + leds.push({ type, on: Math.random() > 0.2, + blinkPeriod: 400 + Math.random() * 5000, + timer: Math.random() * 5000 }); + } + units.push({ leds, powered: Math.random() > 0.08 }); + } + + racks.push({ rx, units, activity, patchRows: 2 + Math.floor(Math.random() * 2) }); + } + return racks; + } + + /* -- paint static geometry to bgCanvas (once per resize) ------------------ */ + function paintBg() { + bgCanvas.width = W; + bgCanvas.height = H; + + /* room background */ + const bg = bgCtx.createLinearGradient(0, 0, 0, H); + bg.addColorStop(0, ROOM_TOP); + bg.addColorStop(0.5, '#05080f'); + bg.addColorStop(1, ROOM_BOT); + bgCtx.fillStyle = bg; + bgCtx.fillRect(0, 0, W, H); + + /* ceiling fluorescent bar */ + const ceil = bgCtx.createLinearGradient(0, 0, 0, H * 0.10); + ceil.addColorStop(0, 'rgba(140,190,255,0.13)'); + ceil.addColorStop(1, 'rgba(140,190,255,0)'); + bgCtx.fillStyle = ceil; + bgCtx.fillRect(0, 0, W, H * 0.10); + + /* floor glow */ + const flr = bgCtx.createLinearGradient(0, H * 0.84, 0, H); + flr.addColorStop(0, 'rgba(0,220,110,0)'); + flr.addColorStop(1, 'rgba(0,180,90,0.10)'); + bgCtx.fillStyle = flr; + bgCtx.fillRect(0, H * 0.84, W, H * 0.16); + + for (const layer of layers) { + const { def, racks } = layer; + const { rackW, unitH, unitGap, railW, padTop, ledSz, ledCols, ledRows, alpha, yShift } = def; + const layerY = H * yShift; + const uw = rackW - 2 * railW; + + bgCtx.save(); + bgCtx.globalAlpha = alpha; + + for (const rack of racks) { + const { rx, units, patchRows } = rack; + const totalRows = units.length + patchRows; + const rackH = totalRows * (unitH + unitGap) - unitGap + padTop * 2 + 6; + const ry = Math.floor((H - rackH) / 2) + layerY; + const ux = rx + railW; + + /* 1. rack shadow (cartoon depth) */ + bgCtx.fillStyle = RACK_SHELL2; + bgCtx.fillRect(rx + 2, ry + 2, rackW, rackH); + + /* 2. rack outer shell */ + bgCtx.fillStyle = RACK_SHELL; + bgCtx.fillRect(rx, ry, rackW, rackH); + + /* cartoon top/left highlights */ + bgCtx.fillStyle = 'rgba(255,255,255,0.13)'; + bgCtx.fillRect(rx, ry, rackW, 1); + bgCtx.fillStyle = 'rgba(255,255,255,0.07)'; + bgCtx.fillRect(rx, ry, 1, rackH); + + /* 3. inner face panel */ + bgCtx.fillStyle = RACK_FACE; + bgCtx.fillRect(ux, ry + 1, uw, rackH - 2); + + /* 4. mounting rail columns */ + [rx, rx + rackW - railW].forEach(function(cx) { + bgCtx.fillStyle = RAIL_FACE; + bgCtx.fillRect(cx, ry, railW, rackH); + + /* inner-edge accent */ + const edgeX = (cx === rx) ? cx + railW - 1 : cx; + bgCtx.fillStyle = RAIL_EDGE; + bgCtx.fillRect(edgeX, ry, 1, rackH); + + /* screw holes */ + const screwX = cx + (railW / 2 | 0) - 1; + const screwSz = Math.max(2, railW * 0.28 | 0); + let sy = ry + padTop; + while (sy < ry + rackH - padTop) { + bgCtx.fillStyle = SCREW_COL; + bgCtx.fillRect(screwX, sy, screwSz, screwSz); + sy += unitH + unitGap; + } + }); + + /* 5. top cap brace */ + bgCtx.fillStyle = 'rgba(255,255,255,0.05)'; + bgCtx.fillRect(ux, ry, uw, padTop); + + /* 6. cable management trough */ + bgCtx.fillStyle = CAB_TROUGH; + bgCtx.fillRect(ux, ry + rackH - padTop - 2, uw, padTop + 2); + bgCtx.fillStyle = 'rgba(255,255,255,0.04)'; + bgCtx.fillRect(ux, ry + rackH - padTop - 2, uw, 1); + + /* 7. patch panel rows */ + for (let p = 0; p < patchRows; p++) { + const py = ry + padTop + p * (unitH + unitGap); + + bgCtx.fillStyle = PATCH_BODY; + bgCtx.fillRect(ux, py, uw, unitH); + + /* top bevel + bottom shadow */ + bgCtx.fillStyle = 'rgba(255,255,255,0.08)'; + bgCtx.fillRect(ux, py, uw, 1); + bgCtx.fillStyle = 'rgba(0,0,0,0.38)'; + bgCtx.fillRect(ux, py + unitH - 1, uw, 1); + + /* label strip (left ~18%) */ + const lblW = Math.max(6, uw * 0.18 | 0); + bgCtx.fillStyle = PATCH_LBL; + bgCtx.fillRect(ux + 2, py + 2, lblW, unitH - 4); + + /* RJ-45 style port holes */ + const portsX = ux + lblW + 4; + const portsW = uw - lblW - 6; + const portW = Math.max(3, unitH * 0.50 | 0); + const portH = Math.max(2, unitH * 0.38 | 0); + const portY = py + ((unitH - portH) / 2 | 0); + const numPort = Math.floor(portsW / (portW + 2)); + bgCtx.fillStyle = PATCH_PORT; + for (let pp = 0; pp < numPort; pp++) { + bgCtx.fillRect(portsX + pp * (portW + 2), portY, portW, portH); + } + } + + /* 8. server units */ + for (let u = 0; u < units.length; u++) { + const unit = units[u]; + const uy = ry + padTop + (u + patchRows) * (unitH + unitGap); + + if (!unit.powered) { + /* empty / powered-off slot */ + bgCtx.fillStyle = SRV_OFF; + bgCtx.fillRect(ux, uy, uw, unitH); + bgCtx.fillStyle = 'rgba(0,0,0,0.32)'; + bgCtx.fillRect(ux, uy + unitH - 1, uw, 1); + continue; + } + + /* server face */ + bgCtx.fillStyle = SRV_FACE; + bgCtx.fillRect(ux, uy, uw, unitH); + + /* top highlight stripe */ + bgCtx.fillStyle = SRV_STRIPE; + bgCtx.fillRect(ux, uy, uw, 1); + + /* bottom shadow line */ + bgCtx.fillStyle = SRV_SHADOW; + bgCtx.fillRect(ux, uy + unitH - 1, uw, 1); + + /* -- power button (left section) -- */ + if (unitH >= 12) { + const pBtnR = Math.max(2, unitH * 0.18 | 0); + const pBtnX = ux + railW + pBtnR + 1; + const pBtnY = uy + (unitH / 2 | 0) - pBtnR; + /* outer ring */ + bgCtx.fillStyle = PWRBTN_RING; + bgCtx.fillRect(pBtnX - 1, pBtnY - 1, pBtnR * 2 + 2, pBtnR * 2 + 2); + /* inner recess */ + bgCtx.fillStyle = PWRBTN_FACE; + bgCtx.fillRect(pBtnX, pBtnY, pBtnR * 2, pBtnR * 2); + } + + /* -- vent / louver slots -- */ + if (unitH >= 14) { + const pBtnR = Math.max(2, unitH * 0.18 | 0); + const ventStartX = ux + railW + pBtnR * 2 + 5; + const ventH = Math.max(3, unitH - 6); + const ventY = uy + 3; + const numVents = Math.min(5, Math.floor(uw * 0.10 / 3)); + bgCtx.fillStyle = VENT_SLOT; + for (let v = 0; v < numVents; v++) { + bgCtx.fillRect(ventStartX + v * 3, ventY, 1, ventH); + } + } + + /* -- drive bays (centre section) -- */ + const ledPanelW = ledCols * (ledSz + 3) + 5; + const rightStop = ux + uw - ledPanelW; + const leftStop = ux + Math.max( + railW * 2 + (unitH * 0.18 | 0) * 2 + (unitH >= 14 ? 14 : 4) + 4, + uw * 0.22 | 0 + ); + const bayAreaW = rightStop - leftStop; + + if (bayAreaW > 8 && unitH >= 10) { + const bayH = Math.max(3, unitH * 0.46 | 0); + const bayW = Math.max(4, Math.min(11, bayAreaW / 5 | 0)); + const bayY = uy + ((unitH - bayH) / 2 | 0); + const numBays = Math.min(8, Math.floor(bayAreaW / (bayW + 2))); + + for (let b = 0; b < numBays; b++) { + const bx = leftStop + b * (bayW + 2); + /* bay recess */ + bgCtx.fillStyle = BAY_SLOT; + bgCtx.fillRect(bx, bayY, bayW, bayH); + /* bay top-edge highlight */ + bgCtx.fillStyle = BAY_EDGE; + bgCtx.fillRect(bx, bayY, bayW, 1); + } + } + + /* -- off-state LED panel (right section) -- */ + const lox = ledOriginX(ux, uw, ledCols, ledSz); + const loy = ledOriginY(uy, unitH, ledRows, ledSz); + /* panel inset background */ + bgCtx.fillStyle = 'rgba(0,0,0,0.28)'; + bgCtx.fillRect(lox - 2, uy + 2, ledCols * (ledSz + 3) + 1, unitH - 4); + /* placeholder dots */ + bgCtx.fillStyle = LED_OFF; + for (let row = 0; row < ledRows; row++) { + for (let col = 0; col < ledCols; col++) { + bgCtx.fillRect( + lox + col * (ledSz + 3), + loy + row * (ledSz + 2), + ledSz, ledSz + ); + } + } + } + } + + bgCtx.restore(); + } + } + + /* -- collect LED state & draw all lit LEDs batched by colour -------------- */ + function drawLEDs(dt) { + const buckets = { green: [], amber: [], red: [], blue: [] }; + + for (const layer of layers) { + const { def, racks } = layer; + const { rackW, unitH, unitGap, railW, padTop, ledSz, ledCols, ledRows, alpha, yShift } = def; + const uw = rackW - 2 * railW; + const layerY = H * yShift; + + for (const rack of racks) { + const { rx, units, activity, patchRows } = rack; + const totalRows = units.length + patchRows; + const rackH = totalRows * (unitH + unitGap) - unitGap + padTop * 2 + 6; + const ry = Math.floor((H - rackH) / 2) + layerY; + const ux = rx + railW; + + /* rack status strip on left rail */ + buckets['green'].push({ + x: rx + 1, y: ry + padTop, w: 2, h: rackH - padTop * 2, + a: alpha * 0.28, + }); + + /* per-unit LEDs */ + for (let u = 0; u < units.length; u++) { + const unit = units[u]; + if (!unit.powered) continue; + + const uy = ry + padTop + (u + patchRows) * (unitH + unitGap); + const lox = ledOriginX(ux, uw, ledCols, ledSz); + const loy = ledOriginY(uy, unitH, ledRows, ledSz); + + for (let row = 0; row < ledRows; row++) { + for (let col = 0; col < ledCols; col++) { + const led = unit.leds[row * ledCols + col]; + led.timer += dt; + if (led.timer >= led.blinkPeriod) { + led.timer = 0; + if (led.type === 'amber' || Math.random() < 0.2) led.on = !led.on; + } + if (!led.on) continue; + buckets[led.type].push({ + x: lox + col * (ledSz + 3), + y: loy + row * (ledSz + 2), + w: ledSz, h: ledSz, a: alpha * 0.92, + }); + } + } + } + } + } + + /* one shadowBlur setup per colour */ + for (const [key, rects] of Object.entries(buckets)) { + if (!rects.length) continue; + const color = LED_COLORS[key]; + ctx.shadowColor = color; + ctx.shadowBlur = 8; + ctx.fillStyle = color; + for (const r of rects) { + ctx.globalAlpha = r.a; + ctx.fillRect(r.x, r.y, r.w, r.h); + } + } + ctx.shadowBlur = 0; + ctx.shadowColor = 'transparent'; + ctx.globalAlpha = 1; + } + + /* -- main render loop ------------------------------------------------------ */ + function draw(ts) { + rafId = requestAnimationFrame(draw); + + const elapsed = ts - lastTs; + lastTs = ts; + fpsDebt += elapsed; + if (fpsDebt < FRAME_MS) return; + const dt = Math.min(fpsDebt, 80); + fpsDebt = fpsDebt % FRAME_MS; + + /* blit static geometry */ + ctx.drawImage(bgCanvas, 0, 0); + + /* mid-aisle ambient glow */ + const aisle = ctx.createLinearGradient(0, H * 0.46, 0, H * 0.58); + aisle.addColorStop(0, 'rgba(0,80,180,0)'); + aisle.addColorStop(0.5, 'rgba(0,80,180,0.022)'); + aisle.addColorStop(1, 'rgba(0,80,180,0)'); + ctx.fillStyle = aisle; + ctx.fillRect(0, H * 0.44, W, H * 0.16); + + /* lit LEDs */ + drawLEDs(dt); + + /* sweeping scan line */ + scanY = (scanY + 0.28 * (dt / 16)) % (H + 200); + const sg = ctx.createLinearGradient(0, scanY - 110, 0, scanY + 110); + sg.addColorStop(0, 'rgba(60,180,255,0)'); + sg.addColorStop(0.45, 'rgba(60,180,255,0.025)'); + sg.addColorStop(0.5, 'rgba(60,180,255,0.06)'); + sg.addColorStop(0.55, 'rgba(60,180,255,0.025)'); + sg.addColorStop(1, 'rgba(60,180,255,0)'); + ctx.fillStyle = sg; + ctx.fillRect(0, scanY - 110, W, 220); + + /* vignette */ + const vig = ctx.createRadialGradient(W/2, H/2, H * 0.25, W/2, H/2, H * 0.88); + vig.addColorStop(0, 'rgba(0,0,0,0)'); + vig.addColorStop(1, 'rgba(0,0,0,0.60)'); + ctx.fillStyle = vig; + ctx.fillRect(0, 0, W, H); + } + + /* -- lifecycle helpers ----------------------------------------------------- */ + function start() { + if (!rafId) { lastTs = performance.now(); rafId = requestAnimationFrame(draw); } + } + function stop() { + cancelAnimationFrame(rafId); rafId = null; + } + + document.addEventListener('visibilitychange', () => { + document.hidden ? stop() : start(); + }); + + if ('IntersectionObserver' in window) { + new IntersectionObserver(entries => { + entries.forEach(e => e.isIntersecting ? start() : stop()); + }, { threshold: 0.01 }).observe(canvas); + } + + window.addEventListener('resize', () => { + canvas.width = canvas.offsetWidth || canvas.parentElement.offsetWidth; + canvas.height = canvas.offsetHeight || canvas.parentElement.offsetHeight; + W = canvas.width; + H = canvas.height; + layers = LAYERS.map(def => ({ def, racks: buildLayer(def) })); + paintBg(); + }, { passive: true }); + + /* -- init ------------------------------------------------------------------ */ + canvas.width = canvas.offsetWidth || canvas.parentElement.offsetWidth; + canvas.height = canvas.offsetHeight || canvas.parentElement.offsetHeight; + W = canvas.width; + H = canvas.height; + layers = LAYERS.map(def => ({ def, racks: buildLayer(def) })); + paintBg(); + start(); +})(); diff --git a/theme/assets/svg/hero-infrastructure.svg b/theme/assets/svg/hero-infrastructure.svg new file mode 100644 index 0000000..97a32be --- /dev/null +++ b/theme/assets/svg/hero-infrastructure.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + CLOUD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SRV-01 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SRV-02 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SRV-03 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All systems online + + + + + + + Monitoring active + + + + + + + 99.9% uptime + + diff --git a/theme/blocks/editor.css b/theme/blocks/editor.css new file mode 100644 index 0000000..061e8c8 --- /dev/null +++ b/theme/blocks/editor.css @@ -0,0 +1,295 @@ +/** + * OTS Theme β€” Editor-specific overrides + * Loaded only inside the Gutenberg block editor. + */ + +/* ── Font variables for editor preview ── */ +.editor-styles-wrapper { + --font-heading: var(--wp--preset--font-family--heading, var(--font-sans)); + --font-sans: var(--wp--preset--font-family--sans, 'Inter', system-ui, -apple-system, sans-serif); +} +.editor-styles-wrapper h1, +.editor-styles-wrapper h2, +.editor-styles-wrapper h3, +.editor-styles-wrapper h4, +.editor-styles-wrapper h5, +.editor-styles-wrapper h6 { + font-family: var(--font-heading, var(--font-sans)); +} + +/* Full-width blocks fill the editor canvas */ +.editor-styles-wrapper [data-type^="oribi/"] { + max-width: none !important; + margin-left: calc(-1 * var(--wp--custom--spacing--outer, 0px)) !important; + margin-right: calc(-1 * var(--wp--custom--spacing--outer, 0px)) !important; +} + +/* Decorative overlays must not eat pointer events */ +.editor-styles-wrapper .hero-bg-grid, +.editor-styles-wrapper .hero-glow, +.editor-styles-wrapper .hero-glow-2 { + pointer-events: none; +} + +/* RichText placeholders inside dark sections */ +.editor-styles-wrapper .hero [data-rich-text-placeholder]::after, +.editor-styles-wrapper .cta-banner [data-rich-text-placeholder]::after, +.editor-styles-wrapper .page-hero [data-rich-text-placeholder]::after { + color: rgba(255, 255, 255, 0.4); +} + +/* Force hero container to be clickable above decorative layers */ +.editor-styles-wrapper .hero .container, +.editor-styles-wrapper .cta-banner .container, +.editor-styles-wrapper .page-hero .container { + position: relative; + z-index: 2; +} + +/* Prevent cursor issues on static elements used as RichText wrappers */ +.editor-styles-wrapper .btn.btn-primary, +.editor-styles-wrapper .btn.btn-ghost { + cursor: text; +} + +/* Inline-editable stat values and labels */ +.editor-styles-wrapper .hero-stat-value[contenteditable], +.editor-styles-wrapper .hero-stat-label[contenteditable] { + cursor: text; + min-width: 40px; + outline: none; +} +.editor-styles-wrapper .hero-stat-value:focus, +.editor-styles-wrapper .hero-stat-label:focus { + outline: 1px dashed rgba(255,255,255,.25); + outline-offset: 2px; + border-radius: 3px; +} + +/* Secondary button placeholder visibility */ +.editor-styles-wrapper .btn.btn-ghost[data-rich-text-placeholder]::after { + color: rgba(255,255,255,.3); +} + +/* Device visual shouldn't eat clicks from editable text */ +.editor-styles-wrapper .hero-devices { + pointer-events: none; +} + +/* Grid / card hover effects are distracting while editing */ +.editor-styles-wrapper .oribi-card:hover, +.editor-styles-wrapper .feature-card:hover, +.editor-styles-wrapper .pricing-card:hover { + transform: none !important; + box-shadow: none !important; +} + +/* Ensure platform-row visuals don't overlap editable text */ +.editor-styles-wrapper .platform-visual { + pointer-events: none; +} + +/* ── Animated hero editor overrides ───────────────────────── */ +.editor-styles-wrapper .hero-animated .hero-particles, +.editor-styles-wrapper .page-hero-animated .hero-particles, +.editor-styles-wrapper .hero-animated__glow { + pointer-events: none; +} + +.editor-styles-wrapper .hero-animated .container, +.editor-styles-wrapper .page-hero-animated .container { + position: relative; + z-index: 2; +} + +.editor-styles-wrapper .hero-animated [data-rich-text-placeholder]::after, +.editor-styles-wrapper .page-hero-animated [data-rich-text-placeholder]::after { + color: rgba(255, 255, 255, 0.4); +} + +/* Contact form placeholder in editor */ +.editor-styles-wrapper .contact-form-wrap { + min-height: 180px; +} + +/* Remove default block outline in favour of section styling */ +.editor-styles-wrapper [data-type^="oribi/"] > .block-editor-block-list__block-edit > [data-block] { + outline: none; +} + +/* ── InnerBlocks child styling ─────────────────────────────────────────── */ + +/* Child blocks inside grids: remove block-list spacing */ +.editor-styles-wrapper [data-type="oribi/feature-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/value-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/addon-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/image-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/stat-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/link-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/pricing-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/platform-section"] .block-editor-inner-blocks, +.editor-styles-wrapper [data-type="oribi/trust-section"] .block-editor-inner-blocks { + width: 100%; +} + +/* Feature cards grid: children lay out inline */ +.editor-styles-wrapper .grid-2 > .block-editor-inner-blocks > .block-editor-block-list__layout, +.editor-styles-wrapper .grid-3 > .block-editor-inner-blocks > .block-editor-block-list__layout, +.editor-styles-wrapper .grid-4 > .block-editor-inner-blocks > .block-editor-block-list__layout { + display: grid; + gap: 2rem; +} +.editor-styles-wrapper .grid-2 > .block-editor-inner-blocks > .block-editor-block-list__layout { + grid-template-columns: repeat(2, 1fr); +} +.editor-styles-wrapper .grid-3 > .block-editor-inner-blocks > .block-editor-block-list__layout { + grid-template-columns: repeat(3, 1fr); +} +.editor-styles-wrapper .grid-4 > .block-editor-inner-blocks > .block-editor-block-list__layout { + grid-template-columns: repeat(4, 1fr); +} + +/* Pricing cards grid */ +.editor-styles-wrapper .pricing-grid > .block-editor-inner-blocks > .block-editor-block-list__layout { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 2rem; +} + +/* Trust items: stack vertically with gap */ +.editor-styles-wrapper [data-type="oribi/trust-section"] .grid-2 > div:first-child .block-editor-block-list__layout { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* ── Oribi Icon Picker ─────────────────────────────────────── */ +.oribi-icon-picker { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 4px; +} +.oribi-icon-current { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + background: #f0f0f1; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; + color: #1e1e1e; + min-height: 36px; +} +.oribi-icon-current i { + font-size: 18px; + flex-shrink: 0; +} +.oribi-icon-current-label { + font-family: monospace; + font-size: 11px; + color: #555; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} +.oribi-icon-clear { + margin-left: auto; + flex-shrink: 0; + background: none; + border: 1px solid #cc1818; + border-radius: 3px; + color: #cc1818; + cursor: pointer; + font-size: 11px; + padding: 2px 6px; + line-height: 1.4; +} +.oribi-icon-clear:hover { + background: #cc1818; + color: #fff; +} +.oribi-icon-search { + width: 100%; + padding: 6px 10px; + border: 1px solid #949494; + border-radius: 4px; + font-size: 13px; + line-height: 1.4; + box-sizing: border-box; + outline: none; +} +.oribi-icon-search:focus { + border-color: #3858e9; + box-shadow: 0 0 0 2px rgba(56, 88, 233, .2); +} +.oribi-icon-count { + font-size: 11px; + color: #757575; + text-align: right; + line-height: 1; +} +.oribi-icon-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 3px; + max-height: 252px; + overflow-y: auto; + padding: 4px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fafafa; +} +.oribi-icon-grid::-webkit-scrollbar { width: 6px; } +.oribi-icon-grid::-webkit-scrollbar-track { background: #f0f0f0; border-radius: 3px; } +.oribi-icon-grid::-webkit-scrollbar-thumb { background: #bbb; border-radius: 3px; } +.oribi-icon-cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + gap: 3px; + padding: 7px 2px 5px; + border: 1px solid transparent; + border-radius: 4px; + background: #fff; + cursor: pointer; + min-width: 0; + transition: background .12s, border-color .12s, color .12s; + color: #1e1e1e; +} +.oribi-icon-cell:hover { + background: #e8f0fe; + border-color: #3858e9; + color: #3858e9; +} +.oribi-icon-cell.is-active { + background: #3858e9; + border-color: #3858e9; + color: #fff; +} +.oribi-icon-cell i { + font-size: 17px; + line-height: 1; +} +.oribi-icon-cell span { + font-size: 8.5px; + color: inherit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + text-align: center; + opacity: 0.75; + line-height: 1.2; +} +.oribi-icon-empty { + font-size: 12px; + color: #757575; + text-align: center; + padding: 16px; + grid-column: 1 / -1; +} diff --git a/theme/blocks/editor.js b/theme/blocks/editor.js new file mode 100644 index 0000000..f11b167 --- /dev/null +++ b/theme/blocks/editor.js @@ -0,0 +1,1774 @@ +/** + * OTS Theme β€” Custom Block Editor Scripts (InnerBlocks Architecture) + * + * 13 blocks: 5 standalone + 4 parent/child pairs. + * Parent blocks use InnerBlocks for child items. + * Child blocks are dynamic (save -> null) with inline RichText editing. + */ +(function (wp) { +'use strict'; + +var el = wp.element.createElement; +var Frag = wp.element.Fragment; +var reg = wp.blocks.registerBlockType; +var RT = wp.blockEditor.RichText; +var IC = wp.blockEditor.InspectorControls; +var IB = wp.blockEditor.InnerBlocks; +var PB = wp.components.PanelBody; +var TC = wp.components.TextControl; +var TA = wp.components.TextareaControl; +var SC = wp.components.SelectControl; +var TG = wp.components.ToggleControl; +var RC = wp.components.RangeControl; +var Btn = wp.components.Button; +var MUC = wp.blockEditor.MediaUploadCheck; +var MU = wp.blockEditor.MediaUpload; +var useS = wp.element.useState; + +/* ── Simple array helpers (for pricing-card features) ────────────────── */ +function arrSet(arr, i, val) { var c = arr.slice(); c[i] = val; return c; } +function arrAdd(arr, val) { return arr.concat([val]); } +function arrRm(arr, i) { var c = arr.slice(); c.splice(i, 1); return c; } + +/* ── Shared card image attributes ────────────────────────────────────── */ +var CARD_IMAGE_ATTRS = { + imgId: { type: 'number', default: 0 }, + imgUrl: { type: 'string', default: '' }, + imgAlt: { type: 'string', default: '' }, + imgWidth: { type: 'number', default: 80 }, + imgHeight: { type: 'number', default: 0 }, + imgFit: { type: 'string', default: 'contain' }, + imgPosition: { type: 'string', default: 'top' } +}; + +/* ── Shared icon attributes ──────────────────────────────────────────── */ +var CARD_ICON_ATTRS = { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' } +}; + +/** + * Build icon InspectorControls: a toggle that switches between emoji and + * Font Awesome mode, with the appropriate text input shown beneath it. + */ + +/* ── Font Awesome icon catalogue ─────────────────────────────────────── */ +// Format: ['s' = fas solid | 'b' = fab brand, 'icon-name'] +var FA_ICONS = [ + // Navigation + ['s','angle-down'],['s','angle-left'],['s','angle-right'],['s','angle-up'], + ['s','angles-down'],['s','angles-left'],['s','angles-right'],['s','angles-up'], + ['s','arrow-down'],['s','arrow-left'],['s','arrow-right'],['s','arrow-up'], + ['s','arrow-rotate-left'],['s','arrow-rotate-right'],['s','arrows-rotate'], + ['s','arrows-up-down-left-right'],['s','caret-down'],['s','caret-left'], + ['s','caret-right'],['s','caret-up'],['s','chevron-down'],['s','chevron-left'], + ['s','chevron-right'],['s','chevron-up'],['s','circle-arrow-down'], + ['s','circle-arrow-left'],['s','circle-arrow-right'],['s','circle-arrow-up'], + // Interface + ['s','asterisk'],['s','at'],['s','ban'],['s','bars'],['s','bell'],['s','bell-slash'], + ['s','bolt'],['s','check'],['s','check-double'],['s','circle-check'], + ['s','circle-exclamation'],['s','circle-info'],['s','circle-minus'], + ['s','circle-plus'],['s','circle-question'],['s','circle-xmark'], + ['s','ellipsis'],['s','ellipsis-vertical'],['s','eye'],['s','eye-slash'], + ['s','filter'],['s','grip'],['s','grip-vertical'],['s','hashtag'],['s','key'], + ['s','lock'],['s','lock-open'],['s','magnifying-glass'],['s','minus'],['s','plus'], + ['s','power-off'],['s','rotate'],['s','shield'],['s','shield-check'], + ['s','shield-halved'],['s','sliders'],['s','sort'],['s','sort-down'],['s','sort-up'], + ['s','spinner'],['s','star'],['s','star-half-stroke'],['s','toggle-off'], + ['s','toggle-on'],['s','xmark'], + // Text editing + ['s','align-center'],['s','align-justify'],['s','align-left'],['s','align-right'], + ['s','bold'],['s','code'],['s','crop'],['s','eraser'],['s','highlight'], + ['s','indent'],['s','italic'],['s','link'],['s','link-slash'],['s','list'], + ['s','list-check'],['s','list-ol'],['s','list-ul'],['s','outdent'],['s','pen'], + ['s','pen-to-square'],['s','pencil'],['s','quote-left'],['s','quote-right'], + ['s','scissors'],['s','strikethrough'],['s','subscript'],['s','superscript'], + ['s','table'],['s','table-list'],['s','underline'], + // Files and documents + ['s','book'],['s','book-open'],['s','bookmark'],['s','box'],['s','box-archive'], + ['s','boxes-stacked'],['s','clipboard'],['s','clipboard-check'], + ['s','clipboard-list'],['s','copy'],['s','database'],['s','download'], + ['s','envelope'],['s','envelope-open'],['s','file'],['s','file-arrow-down'], + ['s','file-arrow-up'],['s','file-code'],['s','file-excel'],['s','file-image'], + ['s','file-lines'],['s','file-pdf'],['s','file-powerpoint'],['s','file-word'], + ['s','file-zipper'],['s','floppy-disk'],['s','folder'],['s','folder-minus'], + ['s','folder-open'],['s','folder-plus'],['s','inbox'],['s','newspaper'], + ['s','paper-plane'],['s','paperclip'],['s','print'],['s','share'], + ['s','share-nodes'],['s','upload'], + // Technology + ['s','barcode'],['s','bug'],['s','cloud'],['s','cloud-arrow-down'], + ['s','cloud-arrow-up'],['s','code-branch'],['s','cpu'],['s','desktop'], + ['s','display'],['s','ethernet'],['s','gear'],['s','gears'],['s','hard-drive'], + ['s','keyboard'],['s','laptop'],['s','memory'],['s','microchip'],['s','mobile'], + ['s','mobile-screen'],['s','network-wired'],['s','plug'],['s','qrcode'], + ['s','robot'],['s','satellite'],['s','satellite-dish'],['s','server'], + ['s','tablet'],['s','terminal'],['s','toolbox'],['s','wifi'],['s','wrench'], + // Media and communication + ['s','backward'],['s','backward-fast'],['s','camera'],['s','camera-retro'], + ['s','clapperboard'],['s','comment'],['s','comment-dots'],['s','comments'], + ['s','film'],['s','forward'],['s','forward-fast'],['s','headphones'], + ['s','message'],['s','microphone'],['s','microphone-slash'],['s','music'], + ['s','pause'],['s','phone'],['s','phone-slash'],['s','phone-volume'], + ['s','photo-film'],['s','play'],['s','radio'],['s','repeat'],['s','rss'], + ['s','shuffle'],['s','stop'],['s','tv'],['s','video'],['s','video-slash'], + ['s','voicemail'],['s','volume-high'],['s','volume-low'],['s','volume-xmark'], + // People + ['s','address-book'],['s','address-card'],['s','baby'],['s','child'], + ['s','circle-user'],['s','people-group'],['s','person'],['s','person-running'], + ['s','person-walking'],['s','thumbs-down'],['s','thumbs-up'],['s','user'], + ['s','user-check'],['s','user-group'],['s','user-minus'],['s','user-plus'], + ['s','user-slash'],['s','user-tie'],['s','users'], + // Location and maps + ['s','building'],['s','buildings'],['s','city'],['s','compass'], + ['s','earth-americas'],['s','earth-asia'],['s','earth-europe'],['s','flag'], + ['s','globe'],['s','house'],['s','location-dot'],['s','location-pin'], + ['s','map'],['s','map-pin'],['s','mountain'],['s','road'],['s','route'], + ['s','signs-post'], + // Business and finance + ['s','award'],['s','briefcase'],['s','bullhorn'],['s','bullseye'], + ['s','calendar'],['s','calendar-check'],['s','calendar-days'], + ['s','calendar-minus'],['s','calendar-plus'],['s','certificate'], + ['s','chart-bar'],['s','chart-column'],['s','chart-line'],['s','chart-pie'], + ['s','chart-simple'],['s','clock'],['s','coins'],['s','credit-card'], + ['s','crown'],['s','gem'],['s','gift'],['s','handshake'],['s','medal'], + ['s','money-bill'],['s','money-bill-wave'],['s','piggy-bank'],['s','receipt'], + ['s','stopwatch'],['s','suitcase'],['s','tag'],['s','tags'],['s','timer'], + ['s','trophy'],['s','truck'],['s','wallet'], + // Nature and weather + ['s','fire'],['s','fire-flame-curved'],['s','leaf'],['s','moon'], + ['s','seedling'],['s','snowflake'],['s','sun'],['s','thermometer'], + ['s','tree'],['s','water'],['s','wind'], + // Health and medical + ['s','bandage'],['s','bone'],['s','brain'],['s','dna'],['s','heart'], + ['s','heart-pulse'],['s','hospital'],['s','pills'],['s','stethoscope'], + ['s','syringe'],['s','tooth'],['s','virus'],['s','wheelchair'], + // Brands + ['b','android'],['b','angular'],['b','apple'],['b','aws'],['b','bitbucket'], + ['b','bootstrap'],['b','chrome'],['b','css3'],['b','discord'],['b','docker'], + ['b','dribbble'],['b','dropbox'],['b','facebook'],['b','facebook-f'], + ['b','figma'],['b','firefox'],['b','git'],['b','github'],['b','gitlab'], + ['b','google'],['b','html5'],['b','instagram'],['b','java'],['b','linkedin'], + ['b','linux'],['b','microsoft'],['b','node-js'],['b','npm'],['b','php'], + ['b','python'],['b','react'],['b','slack'],['b','square-js'],['b','tiktok'], + ['b','vuejs'],['b','whatsapp'],['b','windows'],['b','wordpress'], + ['b','x-twitter'],['b','youtube'] +]; + +/* ── Icon picker component ───────────────────────────────────────────── */ +function IconPicker(props) { + var value = props.value || ''; + var onChange = props.onChange; + var qs = useS(''); + var query = qs[0], setQuery = qs[1]; + + var lower = query.toLowerCase().replace(/\s+/g, '-'); + var filtered = query + ? FA_ICONS.filter(function(ic){ return ic[1].indexOf(lower) !== -1; }) + : FA_ICONS; + + return el('div', { className: 'oribi-icon-picker' }, + // Current selection row + value + ? el('div', { className: 'oribi-icon-current' }, + el('i', { className: value, 'aria-hidden': 'true' }), + el('span', { className: 'oribi-icon-current-label' }, value), + el('button', { + className: 'oribi-icon-clear', + type: 'button', + onClick: function(){ onChange(''); } + }, '\u2715 Clear') + ) + : null, + // Search input + el('input', { + type: 'search', + className: 'oribi-icon-search', + placeholder: 'Search ' + FA_ICONS.length + ' icons…', + value: query, + onChange: function(e){ setQuery(e.target.value); } + }), + // Result count badge (only when filtering) + query + ? el('div', { className: 'oribi-icon-count' }, filtered.length + ' result' + (filtered.length === 1 ? '' : 's')) + : null, + // Icon grid + el('div', { className: 'oribi-icon-grid' }, + filtered.length === 0 + ? el('div', { className: 'oribi-icon-empty' }, 'No icons found.') + : filtered.map(function(ic) { + var prefix = ic[0] === 'b' ? 'fab' : 'fas'; + var cls = prefix + ' fa-' + ic[1]; + var active = cls === value; + return el('button', { + key: cls, + type: 'button', + title: ic[1], + className: 'oribi-icon-cell' + (active ? ' is-active' : ''), + onClick: function(){ onChange(cls); } + }, + el('i', { className: cls, 'aria-hidden': 'true' }), + el('span', null, ic[1]) + ); + }) + ) + ); +} + +function iconControls(a, s) { + var useFa = a.iconType === 'fontawesome'; + return el(Frag, null, + el(TG, { + label: 'Use Font Awesome icon', + checked: useFa, + onChange: function(v){ s({ iconType: v ? 'fontawesome' : 'emoji' }); } + }), + useFa + ? el(IconPicker, { value: a.faIcon || '', onChange: function(v){ s({ faIcon: v }); } }) + : el(TC, { label: 'Icon (emoji)', value: a.icon || '', onChange: function(v){ s({ icon: v }); } }) + ); +} + +/** + * Return the element to render in the card preview for the current icon state. + * cssClass β€” the wrapper class, e.g. 'feature-icon' or 'value-icon' + * extraStyle β€” optional inline style object for the wrapper + */ +function iconPreview(a, cssClass, extraStyle) { + var useFa = a.iconType === 'fontawesome'; + var hasIcon = useFa ? !!a.faIcon : !!a.icon; + if (!hasIcon) return null; + var child = useFa + ? el('i', { className: a.faIcon || '', 'aria-hidden': 'true' }) + : a.icon; + return el('div', { className: cssClass, style: extraStyle || {} }, child); +} + +/** Build shared Card Image InspectorControls panel. */ +function cardImageControls(a, s) { + var imgW = a.imgWidth || 80; + var imgH = a.imgHeight || 0; + var imgPos = a.imgPosition || 'top'; + var imgFit = a.imgFit || 'contain'; + + return el(PB, { title: 'Card Image', initialOpen: false }, + el(MUC, null, + el(MU, { + onSelect: function(media){ s({ imgId: media.id, imgUrl: media.url, imgAlt: media.alt || '' }); }, + allowedTypes: ['image'], + value: a.imgId || 0, + render: function(ref) { + return el(Frag, null, + a.imgUrl ? el('div', { style: { marginBottom: '8px' } }, + el('img', { src: a.imgUrl, style: { maxWidth: '100%', height: 'auto', borderRadius: '4px', display: 'block', marginBottom: '6px' } }), + el(Btn, { variant: 'link', isDestructive: true, onClick: function(){ s({ imgId: 0, imgUrl: '', imgAlt: '' }); } }, 'Remove image') + ) : null, + el(Btn, { onClick: ref.open, variant: 'secondary', __next40pxDefaultSize: true }, + a.imgUrl ? 'Replace image' : 'Select from Media Library') + ); + } + }) + ), + a.imgUrl ? el(Frag, null, + el(RC, { label: 'Width (px)', value: imgW, min: 20, max: 600, step: 4, + onChange: function(v){ s({ imgWidth: v }); } }), + el(RC, { label: 'Height (px) β€” 0 = auto', value: imgH, min: 0, max: 600, step: 4, + onChange: function(v){ s({ imgHeight: v }); } }), + el(TG, { label: 'Scale to fill (cover)', checked: imgFit === 'cover', + onChange: function(v){ s({ imgFit: v ? 'cover' : 'contain' }); } }), + el(SC, { label: 'Position', value: imgPos, options: [ + { label: 'Above content', value: 'top' }, + { label: 'Left of content', value: 'left' }, + { label: 'Replace icon', value: 'replace-icon' }, + { label: 'Background', value: 'background' } + ], onChange: function(v){ s({ imgPosition: v }); } }) + ) : null + ); +} + +/** Build an image preview element for the editor. */ +function cardImagePreview(a) { + if (!a.imgUrl) return null; + var imgW = a.imgWidth || 80; + var imgH = a.imgHeight || 0; + var imgFit = a.imgFit || 'contain'; + var style = { + width: imgW + 'px', maxWidth: '100%', + height: imgH > 0 ? imgH + 'px' : 'auto', + borderRadius: '4px', objectFit: imgFit, display: 'block' + }; + return el('div', { className: 'oribi-card-img-wrap', style: { marginBottom: '1.25rem' } }, + el('img', { src: a.imgUrl, className: 'oribi-card-img oribi-card-img--' + imgFit, style: style }) + ); +} + +/** Build a shared card section edit component. */ +function createCardSectionEdit(allowedBlocks, defaultTemplate, label) { + return function(props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Section Settings' }, + el(SC, { label: 'Background', value: a.variant, options: [ + { label: 'Normal', value: 'normal' }, { label: 'Alternate', value: 'alt' } + ], onChange: function(v){s({variant:v});} }), + el(RC, { label: 'Columns', value: a.columns, min: 1, max: 4, onChange: function(v){s({columns:v});} }), + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: a.variant === 'alt' ? 'section section-alt' : 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'section-header' }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Section heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }) + ), + el('div', { className: 'grid-' + a.columns }, + el(IB, { + allowedBlocks: allowedBlocks, + template: defaultTemplate, + templateLock: false + }) + ) + ) + ) + ); + }; +} + +/** Standard section attributes for JS. */ +var SECTION_ATTRS = { + align: { type: 'string', default: 'full' }, + variant: { type: 'string', default: 'normal' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + lead: { type: 'string', default: '' }, + columns: { type: 'number', default: 3 } +}; + +/* ═══════════════════════════════════════════════════════════════════════ + STANDALONE BLOCKS (unchanged architecture) + ═══════════════════════════════════════════════════════════════════════ */ + +/* 1. HERO ─────────────────────────────────────────────────────────────── */ +reg('oribi/hero', { + title: 'Oribi Hero', + icon: 'cover-image', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + label: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + highlightWord: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + primaryBtnText: { type: 'string', default: 'Get in Touch' }, + primaryBtnUrl: { type: 'string', default: '/contact' }, + secondaryBtnText: { type: 'string', default: '' }, + secondaryBtnUrl: { type: 'string', default: '' }, + stat1Value: { type: 'string', default: '' }, + stat1Label: { type: 'string', default: '' }, + stat2Value: { type: 'string', default: '' }, + stat2Label: { type: 'string', default: '' }, + svcLaptop1: { type: 'string', default: 'Data Backup' }, + svcLaptop2: { type: 'string', default: 'Endpoint Security' }, + svcLaptop3: { type: 'string', default: 'Patch Management' }, + svcCloud1: { type: 'string', default: 'Email Protection' }, + svcCloud2: { type: 'string', default: 'License Management' }, + svcCloud3: { type: 'string', default: 'Cloud Backup' }, + svcDesktop1: { type: 'string', default: 'Network Monitoring' }, + svcDesktop2: { type: 'string', default: 'Threat Detection' }, + svcDesktop3: { type: 'string', default: 'Cloud Management' }, + svcPhone1: { type: 'string', default: 'Mobile Security' }, + svcPhone2: { type: 'string', default: 'Data Encryption' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Highlight' }, + el(TC, { label: 'Word to highlight in title', value: a.highlightWord, onChange: function(v){s({highlightWord:v});} }) + ), + el(PB, { title: 'Primary Button' }, + el(TC, { label: 'URL', value: a.primaryBtnUrl, onChange: function(v){s({primaryBtnUrl:v});} }) + ), + el(PB, { title: 'Secondary Button', initialOpen: false }, + el(TC, { label: 'URL', value: a.secondaryBtnUrl, onChange: function(v){s({secondaryBtnUrl:v});} }) + ) + ), + el('section', { className: 'hero' }, + el('div', { className: 'container hero-inner' }, + el('div', { className: 'hero-content' }, + el(RT, { tagName: 'span', className: 'hero-label', value: a.label, + onChange: function(v){s({label:v});}, placeholder: '\u25CF Label text', allowedFormats: [] }), + el(RT, { tagName: 'h1', className: 'hero-title', value: a.title, + onChange: function(v){s({title:v});}, placeholder: 'Hero title...' }), + el(RT, { tagName: 'p', className: 'hero-description', value: a.description, + onChange: function(v){s({description:v});}, placeholder: 'Description...' }), + el('div', { className: 'btn-group' }, + el(RT, { tagName: 'span', className: 'btn btn-primary btn-lg', value: a.primaryBtnText, + onChange: function(v){s({primaryBtnText:v});}, placeholder: 'Button', allowedFormats: [] }), + el(RT, { tagName: 'span', className: 'btn btn-ghost btn-lg', value: a.secondaryBtnText, + onChange: function(v){s({secondaryBtnText:v});}, placeholder: 'Secondary button', allowedFormats: [] }) + ), + el('div', { className: 'hero-stats' }, + el('div', null, + el(RT, { tagName: 'div', className: 'hero-stat-value', value: a.stat1Value, + onChange: function(v){s({stat1Value:v});}, placeholder: '\u2014', allowedFormats: [] }), + el(RT, { tagName: 'div', className: 'hero-stat-label', value: a.stat1Label, + onChange: function(v){s({stat1Label:v});}, placeholder: 'Stat label', allowedFormats: [] }) + ), + el('div', null, + el(RT, { tagName: 'div', className: 'hero-stat-value', value: a.stat2Value, + onChange: function(v){s({stat2Value:v});}, placeholder: '\u2014', allowedFormats: [] }), + el(RT, { tagName: 'div', className: 'hero-stat-label', value: a.stat2Label, + onChange: function(v){s({stat2Label:v});}, placeholder: 'Stat label', allowedFormats: [] }) + ) + ) + ), + el('div', { className: 'hero-visual' }, + el('div', { className: 'hero-devices' }, + el('div', { className: 'hero-device hero-device--laptop', style: { opacity: 1 } }, + el('div', { className: 'hero-device__frame' }, + el('div', { className: 'hero-device__screen' }, + el('div', { className: 'hero-device__screen-content' }, + el('div', { className: 'hero-device__app-bars' }, el('div'),el('div'),el('div'),el('div')) + ) + ), + el('div', { className: 'hero-device__base' }) + ), + el('ul', { className: 'hero-device__services' }, + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Data Backup'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Endpoint Security'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Patch Management') + ) + ), + el('div', { className: 'hero-device hero-device--cloud', style: { opacity: 1 } }, + el('div', { className: 'hero-device__frame' }, + el('div', { className: 'hero-device__cloud-icon' }, + el('span', { className: 'hero-device__cloud-label' }, '365') + ) + ), + el('ul', { className: 'hero-device__services' }, + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Email Protection'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' License Management'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Cloud Backup') + ) + ), + el('div', { className: 'hero-device hero-device--desktop', style: { opacity: 1 } }, + el('div', { className: 'hero-device__frame' }, + el('div', { className: 'hero-device__screen' }, + el('div', { className: 'hero-device__screen-content' }, + el('div', { className: 'hero-device__dash-row' }, el('div',{className:'hero-device__dash-card'}), el('div',{className:'hero-device__dash-card'})), + el('div', { className: 'hero-device__dash-bar' }), + el('div', { className: 'hero-device__dash-bar hero-device__dash-bar--short' }) + ) + ), + el('div', { className: 'hero-device__stand' }), + el('div', { className: 'hero-device__stand-base' }) + ), + el('ul', { className: 'hero-device__services' }, + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Network Monitoring'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Threat Detection'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Cloud Management') + ) + ), + el('div', { className: 'hero-device hero-device--phone', style: { opacity: 1 } }, + el('div', { className: 'hero-device__frame' }, + el('div', { className: 'hero-device__screen' }, + el('div', { className: 'hero-device__screen-content' }, + el('div', { className: 'hero-device__notif' }, + el('span', { className: 'hero-device__notif-icon' }, '\u2713'), + el('span', { className: 'hero-device__notif-text' }, 'Secure') + ) + ) + ) + ), + el('ul', { className: 'hero-device__services' }, + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Mobile Security'), + el('li', { className: 'svc', style: { opacity: 1 } }, el('span',{className:'svc__dot'}), ' Data Encryption') + ) + ) + ) + ) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* 2. PAGE HERO ────────────────────────────────────────────────────────── */ +reg('oribi/page-hero', { + title: 'Oribi Page Hero', + icon: 'flag', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + label: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Settings' }, + el(TC, { label: 'Label (optional)', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: 'page-hero' }, + el('div', { className: 'container' }, + a.label ? el('span', { className: 'hero-label' }, a.label) : null, + el(RT, { tagName: 'h1', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Page title...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* 5. CTA BANNER ───────────────────────────────────────────────────────── */ +reg('oribi/cta-banner', { + title: 'Oribi CTA Banner', + icon: 'megaphone', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + heading: { type: 'string', default: '' }, + text: { type: 'string', default: '' }, + btnText: { type: 'string', default: '' }, + btnUrl: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Button' }, + el(TC, { label: 'URL', value: a.btnUrl, onChange: function(v){s({btnUrl:v});} }) + ) + ), + el('section', { className: 'cta-banner' }, + el('div', { className: 'container text-center' }, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'CTA heading...' }), + el(RT, { tagName: 'p', value: a.text, onChange: function(v){s({text:v});}, placeholder: 'CTA text...' }), + el(RT, { tagName: 'span', className: 'btn btn-primary btn-lg', style: { background: '#fff', color: 'var(--color-primary)' }, value: a.btnText, onChange: function(v){s({btnText:v});}, placeholder: 'Button text...' }) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* 6. INTRO SECTION ────────────────────────────────────────────────────── */ +reg('oribi/intro-section', { + title: 'Oribi Intro Section', + icon: 'id', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + variant: { type: 'string', default: 'normal' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + visual: { type: 'string', default: '' }, + reversed: { type: 'boolean', default: false }, + imgId: { type: 'number', default: 0 }, + imgUrl: { type: 'string', default: '' }, + imgAlt: { type: 'string', default: '' }, + imgWidth: { type: 'number', default: 280 }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgW = a.imgWidth || 280; + var visualContent = a.imgUrl + ? el('img', { src: a.imgUrl, style: { width: imgW + 'px', maxWidth: '100%', height: 'auto', borderRadius: '8px', objectFit: 'contain', display: 'block' } }) + : (a.visual || '\uD83D\uDCBB'); + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Settings' }, + el(SC, { label: 'Background', value: a.variant, options: [ + { label: 'Normal', value: 'normal' }, { label: 'Alternate', value: 'alt' } + ], onChange: function(v){s({variant:v});} }), + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }), + el(TC, { label: 'Visual (emoji or text)', value: a.visual, onChange: function(v){s({visual:v});} }), + el(TG, { label: 'Reversed layout', checked: a.reversed, onChange: function(v){s({reversed:v});} }) + ), + el(PB, { title: 'Visual Image', initialOpen: false }, + el(MUC, null, + el(MU, { + onSelect: function(media){ s({ imgId: media.id, imgUrl: media.url, imgAlt: media.alt || '' }); }, + allowedTypes: ['image'], + value: a.imgId || 0, + render: function(ref) { + return el(Frag, null, + a.imgUrl ? el('div', { style: { marginBottom: '8px' } }, + el('img', { src: a.imgUrl, style: { maxWidth: '100%', height: 'auto', borderRadius: '4px', display: 'block', marginBottom: '6px' } }), + el(Btn, { variant: 'link', isDestructive: true, onClick: function(){ s({ imgId: 0, imgUrl: '', imgAlt: '' }); } }, 'Remove image') + ) : null, + el(Btn, { onClick: ref.open, variant: 'secondary', __next40pxDefaultSize: true }, + a.imgUrl ? 'Replace image' : 'Select from Media Library') + ); + } + }) + ), + a.imgUrl ? el(RC, { label: 'Width (px)', value: imgW, min: 50, max: 420, step: 4, + onChange: function(v){ s({ imgWidth: v }); } }) : null + ) + ), + el('section', { className: a.variant === 'alt' ? 'section section-alt' : 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'about-intro', style: a.reversed ? { direction: 'rtl' } : {} }, + el('div', { style: a.reversed ? { direction: 'ltr' } : {} }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', style: { marginBottom: '1.5rem' }, value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ), + el('div', { className: 'about-intro-visual' + (a.imgUrl ? ' has-img' : ''), style: a.reversed ? { direction: 'ltr' } : {} }, visualContent) + ) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* 8. CONTACT SECTION ──────────────────────────────────────────────────── */ +reg('oribi/contact-section', { + title: 'Oribi Contact', + icon: 'email', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + heading: { type: 'string', default: "Let's Talk" }, + lead: { type: 'string', default: '' }, + email: { type: 'string', default: 'solutions@oribi-tech.com' }, + supportUrl: { type: 'string', default: '' }, + portalUrl: { type: 'string', default: '' }, + location: { type: 'string', default: 'Saratoga Springs, Upstate New York' }, + formHeading: { type: 'string', default: 'Want to Learn More?' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Contact Settings' }, + el(TC, { label: 'Email', value: a.email, onChange: function(v){s({email:v});} }), + el(TC, { label: 'Support URL', value: a.supportUrl, onChange: function(v){s({supportUrl:v});} }), + el(TC, { label: 'Portal URL', value: a.portalUrl, onChange: function(v){s({portalUrl:v});} }), + el(TC, { label: 'Location', value: a.location, onChange: function(v){s({location:v});} }), + el(TC, { label: 'Form Heading', value: a.formHeading, onChange: function(v){s({formHeading:v});} }) + ) + ), + el('section', { className: 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'contact-layout' }, + el('div', { className: 'contact-info' }, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }), + el('div', { className: 'contact-method' }, + el('div',{className:'contact-method-icon'},'\uD83D\uDCE7'), + el('div',null, el('div',{className:'contact-method-label'},'Email Us'), el('div',{className:'contact-method-value'}, a.email)) + ), + el('div', { className: 'contact-method' }, + el('div',{className:'contact-method-icon'},'\uD83C\uDFAB'), + el('div',null, el('div',{className:'contact-method-label'},'Support'), el('div',{className:'contact-method-value'}, 'Open a Support Ticket')) + ), + el('div', { className: 'contact-method' }, + el('div',{className:'contact-method-icon'},'\uD83C\uDF0E'), + el('div',null, el('div',{className:'contact-method-label'},'Client Portal'), el('div',{className:'contact-method-value'}, 'portal.oribi-tech.com')) + ), + el('div', { className: 'contact-method' }, + el('div',{className:'contact-method-icon'},'\uD83D\uDCCD'), + el('div',null, el('div',{className:'contact-method-label'},'Location'), el('div',{className:'contact-method-value'}, a.location)) + ) + ), + el('div', { className: 'contact-form-wrap' }, + el('h3', { style: { marginBottom: '1.5rem' } }, a.formHeading), + el('div', { style: { padding: '2rem', background: 'var(--color-bg-alt)', borderRadius: 'var(--radius-md)', textAlign: 'center', color: 'var(--color-text-muted)' } }, + 'Contact form renders on the live site' + ) + ) + ) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* ═══════════════════════════════════════════════════════════════════════ + CHILD BLOCKS (each renders one item inside a parent) + ═══════════════════════════════════════════════════════════════════════ */ + +/* ── Feature Card ─────────────────────────────────────────────────────── */ +reg('oribi/feature-card', { + title: 'Feature Card', + icon: 'screenoptions', + category: 'oribi', + parent: ['oribi/feature-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + url: { type: 'string', default: '' }, + centered: { type: 'boolean', default: false } + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPos = a.imgPosition || 'top'; + var imgPrev = cardImagePreview(a); + var centeredStyle = a.centered ? { marginInline: 'auto' } : {}; + + var cardPreview; + if ( a.imgUrl && imgPos === 'left' ) { + cardPreview = el('div', { className: 'oribi-card img-left' }, + imgPrev, + el('div', { className: 'oribi-card-body' }, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Card title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Card description...' }) + ) + ); + } else if ( a.imgUrl && imgPos === 'background' ) { + cardPreview = el('div', { className: 'oribi-card img-bg', style: { backgroundImage: 'url(' + a.imgUrl + ')', backgroundSize: 'cover', backgroundPosition: 'center', color: '#fff' } }, + el('div', { className: 'oribi-card-body' }, + iconPreview(a, 'feature-icon', centeredStyle), + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Card title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Card description...' }) + ) + ); + } else { + var showIconPrev = !a.imgUrl || imgPos !== 'replace-icon'; + cardPreview = el('div', { className: 'oribi-card' + (a.centered ? ' text-center' : '') + (a.imgUrl ? ' img-' + imgPos : '') }, + a.imgUrl && (imgPos === 'top' || imgPos === 'replace-icon') ? imgPrev : null, + showIconPrev ? iconPreview(a, 'feature-icon', centeredStyle) : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Card title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Card description...' }) + ); + } + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s), + el(TC, { label: 'URL (optional)', value: a.url || '', onChange: function(v){s({url:v});} }), + el(TG, { label: 'Centered', checked: !!a.centered, onChange: function(v){s({centered:v});} }) + ), + cardImageControls(a, s) + ), + cardPreview + ); + }, + save: function () { return null; } +}); + +/* ── Value Card ───────────────────────────────────────────────────────── */ +reg('oribi/value-card', { + title: 'Value Card', + icon: 'heart', + category: 'oribi', + parent: ['oribi/value-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' } + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPos = a.imgPosition || 'top'; + var imgPrev = cardImagePreview(a); + var showIconPrev = !a.imgUrl || imgPos !== 'replace-icon'; + + var body; + if ( a.imgUrl && imgPos === 'left' ) { + body = el('div', { className: 'oribi-card value-card img-left' }, + imgPrev, + el('div', { className: 'oribi-card-body' }, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ); + } else if ( a.imgUrl && imgPos === 'background' ) { + body = el('div', { className: 'oribi-card value-card img-bg', style: { backgroundImage: 'url(' + a.imgUrl + ')', backgroundSize: 'cover', backgroundPosition: 'center', color: '#fff' } }, + el('div', { className: 'oribi-card-body' }, + showIconPrev ? iconPreview(a, 'value-icon') : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ); + } else { + body = el('div', { className: 'oribi-card value-card' + (a.imgUrl ? ' img-' + imgPos : '') }, + a.imgUrl && (imgPos === 'top' || imgPos === 'replace-icon') ? imgPrev : null, + showIconPrev ? iconPreview(a, 'value-icon') : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ); + } + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s) + ), + cardImageControls(a, s) + ), + body + ); + }, + save: function () { return null; } +}); + +/* ── Addon Card ───────────────────────────────────────────────────────── */ +reg('oribi/addon-card', { + title: 'Addon Card', + icon: 'plus-alt2', + category: 'oribi', + parent: ['oribi/addon-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + tag: { type: 'string', default: '' } + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPos = a.imgPosition || 'top'; + var imgPrev = cardImagePreview(a); + var showIconPrev = !a.imgUrl || imgPos !== 'replace-icon'; + + var body; + if ( a.imgUrl && imgPos === 'left' ) { + body = el('div', { className: 'oribi-card img-left' }, + imgPrev, + el('div', { className: 'oribi-card-body' }, + a.tag ? el('span', { className: 'addon-tag' }, a.tag) : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ); + } else if ( a.imgUrl && imgPos === 'background' ) { + body = el('div', { className: 'oribi-card img-bg', style: { backgroundImage: 'url(' + a.imgUrl + ')', backgroundSize: 'cover', backgroundPosition: 'center', color: '#fff' } }, + el('div', { className: 'oribi-card-body' }, + showIconPrev ? iconPreview(a, 'feature-icon') : null, + a.tag ? el('span', { className: 'addon-tag' }, a.tag) : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ); + } else { + body = el('div', { className: 'oribi-card' + (a.imgUrl ? ' img-' + imgPos : '') }, + a.imgUrl && (imgPos === 'top' || imgPos === 'replace-icon') ? imgPrev : null, + showIconPrev ? iconPreview(a, 'feature-icon') : null, + a.tag ? el('span', { className: 'addon-tag' }, a.tag) : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ); + } + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s), + el(TC, { label: 'Tag / Badge (optional)', value: a.tag || '', onChange: function(v){s({tag:v});} }) + ), + cardImageControls(a, s) + ), + body + ); + }, + save: function () { return null; } +}); + +/* ── Image Card ───────────────────────────────────────────────────────── */ +reg('oribi/image-card', { + title: 'Image Card', + icon: 'format-image', + category: 'oribi', + parent: ['oribi/image-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + url: { type: 'string', default: '' } + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPos = a.imgPosition || 'top'; + var imgPrev = cardImagePreview(a); + var showIconPrev = !a.imgUrl || imgPos !== 'replace-icon'; + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s), + el(TC, { label: 'URL (optional)', value: a.url || '', onChange: function(v){s({url:v});} }) + ), + cardImageControls(a, s) + ), + el('div', { className: 'oribi-card image-card' + (a.imgUrl ? ' img-' + imgPos : '') }, + a.imgUrl && (imgPos === 'top' || imgPos === 'replace-icon') ? imgPrev : null, + showIconPrev ? iconPreview(a, 'feature-icon') : null, + el('div', { className: 'oribi-card-body' }, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* ── Stat Card ────────────────────────────────────────────────────────── */ +reg('oribi/stat-card', { + title: 'Stat Card', + icon: 'chart-bar', + category: 'oribi', + parent: ['oribi/stat-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + value: { type: 'string', default: '' }, + label: { type: 'string', default: '' }, + description: { type: 'string', default: '' } + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPrev = cardImagePreview(a); + var imgPos = a.imgPosition || 'top'; + + var body; + if ( a.imgUrl && imgPos === 'background' ) { + body = el('div', { className: 'oribi-card stat-card img-bg', style: { backgroundImage: 'url(' + a.imgUrl + ')', backgroundSize: 'cover', backgroundPosition: 'center', color: '#fff' } }, + el('div', { className: 'oribi-card-body' }, + iconPreview(a, 'feature-icon'), + el(RT, { tagName: 'div', className: 'stat-value', value: a.value, onChange: function(v){s({value:v});}, placeholder: '99.9%' }), + el(RT, { tagName: 'div', className: 'stat-label', value: a.label, onChange: function(v){s({label:v});}, placeholder: 'Label...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description (optional)...' }) + ) + ); + } else { + body = el('div', { className: 'oribi-card stat-card' + (a.imgUrl ? ' img-' + imgPos : '') }, + (a.imgUrl && imgPos !== 'replace-icon') ? imgPrev : null, + iconPreview(a, 'feature-icon'), + el(RT, { tagName: 'div', className: 'stat-value', value: a.value, onChange: function(v){s({value:v});}, placeholder: '99.9%' }), + el(RT, { tagName: 'div', className: 'stat-label', value: a.label, onChange: function(v){s({label:v});}, placeholder: 'Label...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description (optional)...' }) + ); + } + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s) + ), + cardImageControls(a, s) + ), + body + ); + }, + save: function () { return null; } +}); + +/* ── Link Card ────────────────────────────────────────────────────────── */ +reg('oribi/link-card', { + title: 'Link Card', + icon: 'admin-links', + category: 'oribi', + parent: ['oribi/link-section'], + supports: { html: false, reusable: false }, + attributes: Object.assign({}, { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + linkText: { type: 'string', default: '' }, + linkUrl: { type: 'string', default: '' } + }, CARD_IMAGE_ATTRS), + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgPos = a.imgPosition || 'top'; + var imgPrev = cardImagePreview(a); + var showIconPrev = !a.imgUrl || imgPos !== 'replace-icon'; + + var body; + if ( a.imgUrl && imgPos === 'left' ) { + body = el('div', { className: 'oribi-card link-card img-left' }, + imgPrev, + el('div', { className: 'oribi-card-body' }, + showIconPrev ? iconPreview(a, 'feature-icon') : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }), + el(RT, { tagName: 'span', className: 'link-card-cta', value: a.linkText, onChange: function(v){s({linkText:v});}, placeholder: 'Link text β†’ ...' }) + ) + ); + } else if ( a.imgUrl && imgPos === 'background' ) { + body = el('div', { className: 'oribi-card link-card img-bg', style: { backgroundImage: 'url(' + a.imgUrl + ')', backgroundSize: 'cover', backgroundPosition: 'center', color: '#fff' } }, + el('div', { className: 'oribi-card-body' }, + showIconPrev ? iconPreview(a, 'feature-icon') : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }), + el(RT, { tagName: 'span', className: 'link-card-cta', value: a.linkText, onChange: function(v){s({linkText:v});}, placeholder: 'Link text β†’ ...' }) + ) + ); + } else { + body = el('div', { className: 'oribi-card link-card' + (a.imgUrl ? ' img-' + imgPos : '') }, + (a.imgUrl && (imgPos === 'top' || imgPos === 'replace-icon')) ? imgPrev : null, + showIconPrev ? iconPreview(a, 'feature-icon') : null, + el(RT, { tagName: 'h3', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Title...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }), + el(RT, { tagName: 'span', className: 'link-card-cta', value: a.linkText, onChange: function(v){s({linkText:v});}, placeholder: 'Link text β†’ ...' }) + ); + } + + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s), + el(TC, { label: 'Link URL', value: a.linkUrl || '', onChange: function(v){s({linkUrl:v});} }) + ), + cardImageControls(a, s) + ), + body + ); + }, + save: function () { return null; } +}); + +/* ── Pricing Card ─────────────────────────────────────────────────────── */ +reg('oribi/pricing-card', { + title: 'Pricing Card', + icon: 'money-alt', + category: 'oribi', + parent: ['oribi/pricing-section'], + supports: { html: false, reusable: false }, + attributes: { + icon: { type: 'string', default: '' }, + iconType: { type: 'string', default: 'emoji' }, + faIcon: { type: 'string', default: '' }, + name: { type: 'string', default: '' }, + tagline: { type: 'string', default: '' }, + features: { type: 'array', default: [] }, + btnText: { type: 'string', default: 'Get Started' }, + btnUrl: { type: 'string', default: '/contact' }, + featured: { type: 'boolean', default: false }, + badge: { type: 'string', default: '' }, + imgId: { type: 'number', default: 0 }, + imgUrl: { type: 'string', default: '' }, + imgAlt: { type: 'string', default: '' }, + imgWidth: { type: 'number', default: 80 }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var features = a.features || []; + var imgW = a.imgWidth || 80; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Card Settings' }, + iconControls(a, s), + el(TC, { label: 'Button URL', value: a.btnUrl, onChange: function(v){s({btnUrl:v});} }), + el(TG, { label: 'Featured', checked: !!a.featured, onChange: function(v){s({featured:v});} }), + a.featured ? el(TC, { label: 'Badge Text', value: a.badge, onChange: function(v){s({badge:v});} }) : null + ), + el(PB, { title: 'Card Image', initialOpen: false }, + el(MUC, null, + el(MU, { + onSelect: function(media){ s({ imgId: media.id, imgUrl: media.url, imgAlt: media.alt || '' }); }, + allowedTypes: ['image'], + value: a.imgId || 0, + render: function(ref) { + return el(Frag, null, + a.imgUrl ? el('div', { style: { marginBottom: '8px' } }, + el('img', { src: a.imgUrl, style: { maxWidth: '100%', height: 'auto', borderRadius: '4px', display: 'block', marginBottom: '6px' } }), + el(Btn, { variant: 'link', isDestructive: true, onClick: function(){ s({ imgId: 0, imgUrl: '', imgAlt: '' }); } }, 'Remove image') + ) : null, + el(Btn, { onClick: ref.open, variant: 'secondary', __next40pxDefaultSize: true }, + a.imgUrl ? 'Replace image' : 'Select from Media Library') + ); + } + }) + ), + a.imgUrl ? el(RC, { label: 'Width (px)', value: imgW, min: 20, max: 400, step: 4, + onChange: function(v){ s({ imgWidth: v }); } }) : null + ) + ), + el('div', { className: 'pricing-card' + (a.featured ? ' featured' : '') }, + a.featured && a.badge ? el(RT, { tagName: 'span', className: 'pricing-badge', value: a.badge, + onChange: function(v){s({badge:v});}, placeholder: 'Badge...' }) : null, + a.imgUrl ? el('div', { style: { textAlign: 'center', marginBottom: '1.25rem' } }, + el('img', { src: a.imgUrl, style: { width: imgW + 'px', maxWidth: '100%', height: 'auto', borderRadius: '4px', objectFit: 'contain' } }) + ) : null, + iconPreview(a, 'feature-icon', { marginInline: 'auto' }), + el(RT, { tagName: 'div', className: 'pricing-name', value: a.name, + onChange: function(v){s({name:v});}, placeholder: 'Plan name...' }), + el(RT, { tagName: 'p', className: 'pricing-tagline', value: a.tagline, + onChange: function(v){s({tagline:v});}, placeholder: 'Tagline...' }), + el('ul', { className: 'pricing-features' }, + features.map(function (f, fi) { + return el('li', { key: fi, style: { display: 'flex', alignItems: 'center', gap: '4px' } }, + el('span', { className: 'pricing-check' }, '\u2713'), + el(RT, { tagName: 'span', style: { flex: 1, minWidth: 0 }, value: f, + onChange: function(v){ s({features: arrSet(features, fi, v)}); }, placeholder: 'Feature...' }), + el(Btn, { isSmall: true, isDestructive: true, + style: { minWidth: '20px', padding: 0, height: '20px', flexShrink: 0 }, + onClick: function(){ s({features: arrRm(features, fi)}); } }, '\u2715') + ); + }), + el('li', { style: { listStyle: 'none', marginTop: '4px' } }, + el(Btn, { isSmall: true, variant: 'secondary', + onClick: function(){ s({features: arrAdd(features, '')}); } }, '+ Feature') + ) + ), + el(RT, { tagName: 'span', + className: 'btn ' + (a.featured ? 'btn-primary' : 'btn-outline'), + style: { width: '100%', justifyContent: 'center', cursor: 'text' }, + value: a.btnText || '', + onChange: function(v){s({btnText:v});}, placeholder: 'Button text...' }) + ) + ); + }, + save: function () { return null; } +}); + +/* ── Platform Row ─────────────────────────────────────────────────────── */ +reg('oribi/platform-row', { + title: 'Platform Row', + icon: 'slides', + category: 'oribi', + parent: ['oribi/platform-section'], + supports: { html: false, reusable: false }, + attributes: { + heading: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + btnText: { type: 'string', default: 'Learn More' }, + btnUrl: { type: 'string', default: '' }, + visual: { type: 'string', default: '' }, + reversed: { type: 'boolean', default: false }, + imgId: { type: 'number', default: 0 }, + imgUrl: { type: 'string', default: '' }, + imgAlt: { type: 'string', default: '' }, + imgWidth: { type: 'number', default: 300 }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var imgW = a.imgWidth || 300; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Row Settings' }, + el(TC, { label: 'Visual (emoji)', value: a.visual, onChange: function(v){s({visual:v});} }), + el(TC, { label: 'Button URL', value: a.btnUrl, onChange: function(v){s({btnUrl:v});} }), + el(TG, { label: 'Reversed', checked: !!a.reversed, onChange: function(v){s({reversed:v});} }) + ), + el(PB, { title: 'Visual Image', initialOpen: false }, + el(MUC, null, + el(MU, { + onSelect: function(media){ s({ imgId: media.id, imgUrl: media.url, imgAlt: media.alt || '' }); }, + allowedTypes: ['image'], + value: a.imgId || 0, + render: function(ref) { + return el(Frag, null, + a.imgUrl ? el('div', { style: { marginBottom: '8px' } }, + el('img', { src: a.imgUrl, style: { maxWidth: '100%', height: 'auto', borderRadius: '4px', display: 'block', marginBottom: '6px' } }), + el(Btn, { variant: 'link', isDestructive: true, onClick: function(){ s({ imgId: 0, imgUrl: '', imgAlt: '' }); } }, 'Remove image') + ) : null, + el(Btn, { onClick: ref.open, variant: 'secondary', __next40pxDefaultSize: true }, + a.imgUrl ? 'Replace image' : 'Select from Media Library') + ); + } + }) + ), + a.imgUrl ? el(RC, { label: 'Width (px)', value: imgW, min: 50, max: 600, step: 4, + onChange: function(v){ s({ imgWidth: v }); } }) : null + ) + ), + el('div', { className: 'platform-row' + (a.reversed ? ' reverse' : '') }, + el('div', { className: 'platform-text' }, + el(RT, { tagName: 'h3', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Service name...' }), + el(RT, { tagName: 'p', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Service description...' }), + a.btnUrl ? el(RT, { tagName: 'span', className: 'btn btn-outline mt-3', + value: a.btnText, onChange: function(v){s({btnText:v});}, placeholder: 'Button...' }) : null + ), + a.imgUrl + ? el('div', { className: 'platform-visual has-img' }, + el('img', { src: a.imgUrl, style: { width: imgW + 'px', maxWidth: '100%', height: 'auto', borderRadius: '4px', objectFit: 'contain', display: 'block', marginInline: 'auto' } }) + ) + : el('div', { className: 'platform-visual' }, a.visual || '\uD83D\uDCBB') + ) + ); + }, + save: function () { return null; } +}); + +/* ── Trust Item ───────────────────────────────────────────────────────── */ +reg('oribi/trust-item', { + title: 'Trust Item', + icon: 'shield', + category: 'oribi', + parent: ['oribi/trust-section'], + supports: { html: false, reusable: false }, + attributes: { + heading: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el('div', { className: 'trust-item' }, + el(RT, { tagName: 'h3', style: { marginBottom: '1rem' }, + value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Sub-heading...' }), + el(RT, { tagName: 'p', value: a.description, + onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ); + }, + save: function () { return null; } +}); + +/* ═══════════════════════════════════════════════════════════════════════ + PARENT BLOCKS (use InnerBlocks for child items) + ═══════════════════════════════════════════════════════════════════════ */ + +/* 3. FEATURE SECTION ──────────────────────────────────────────────────── */ +reg('oribi/feature-section', { + title: 'Oribi Feature Section', + icon: 'grid-view', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/feature-card'], [['oribi/feature-card', {}]], 'Feature Card'), + save: function () { return el(IB.Content); } +}); + +/* VALUE SECTION ────────────────────────────────────────────────────────── */ +reg('oribi/value-section', { + title: 'Oribi Value Section', + icon: 'heart', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/value-card'], [['oribi/value-card', {}]], 'Value Card'), + save: function () { return el(IB.Content); } +}); + +/* ADDON SECTION ────────────────────────────────────────────────────────── */ +reg('oribi/addon-section', { + title: 'Oribi Addon Section', + icon: 'plus-alt2', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/addon-card'], [['oribi/addon-card', {}]], 'Addon Card'), + save: function () { return el(IB.Content); } +}); + +/* IMAGE SECTION ────────────────────────────────────────────────────────── */ +reg('oribi/image-section', { + title: 'Oribi Image Section', + icon: 'format-image', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/image-card'], [['oribi/image-card', {}]], 'Image Card'), + save: function () { return el(IB.Content); } +}); + +/* STAT SECTION ─────────────────────────────────────────────────────────── */ +reg('oribi/stat-section', { + title: 'Oribi Stat Section', + icon: 'chart-bar', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/stat-card'], [['oribi/stat-card', {}]], 'Stat Card'), + save: function () { return el(IB.Content); } +}); + +/* LINK SECTION ─────────────────────────────────────────────────────────── */ +reg('oribi/link-section', { + title: 'Oribi Link Section', + icon: 'admin-links', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: SECTION_ATTRS, + edit: createCardSectionEdit(['oribi/link-card'], [['oribi/link-card', {}]], 'Link Card'), + save: function () { return el(IB.Content); } +}); + +/* 4. PRICING SECTION ──────────────────────────────────────────────────── */ +reg('oribi/pricing-section', { + title: 'Oribi Pricing', + icon: 'money-alt', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + variant: { type: 'string', default: 'normal' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + lead: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Section Settings' }, + el(SC, { label: 'Background', value: a.variant, options: [ + { label: 'Normal', value: 'normal' }, { label: 'Alternate', value: 'alt' } + ], onChange: function(v){s({variant:v});} }), + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: a.variant === 'alt' ? 'section section-alt' : 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'section-header' }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Pricing heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }) + ), + el('div', { className: 'pricing-grid' }, + el(IB, { + allowedBlocks: ['oribi/pricing-card'], + template: [ + ['oribi/pricing-card', { name: 'Essentials' }], + ['oribi/pricing-card', { name: 'Pro', featured: true, badge: 'Most Popular' }], + ['oribi/pricing-card', { name: 'Enterprise' }] + ], + templateLock: false + }) + ) + ) + ) + ); + }, + save: function () { return el(IB.Content); } +}); + +/* 7. PLATFORM SECTION ─────────────────────────────────────────────────── */ +reg('oribi/platform-section', { + title: 'Oribi Platform Section', + icon: 'slides', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + lead: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Section' }, + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'section-header' }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Section heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }) + ), + el(IB, { + allowedBlocks: ['oribi/platform-row'], + template: [['oribi/platform-row', {}]], + templateLock: false + }) + ) + ) + ); + }, + save: function () { return el(IB.Content); } +}); + +/* 9. TRUST SECTION ────────────────────────────────────────────────────── */ +reg('oribi/trust-section', { + title: 'Oribi Trust Section', + icon: 'shield', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + lead: { type: 'string', default: '' }, + btnText: { type: 'string', default: '' }, + btnUrl: { type: 'string', default: '' }, + btnSub: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Settings' }, + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }), + el(TC, { label: 'Button Text', value: a.btnText, onChange: function(v){s({btnText:v});} }), + el(TC, { label: 'Button URL', value: a.btnUrl, onChange: function(v){s({btnUrl:v});} }), + el(TC, { label: 'Button Subtext', value: a.btnSub, onChange: function(v){s({btnSub:v});} }) + ) + ), + el('section', { className: 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'section-header' }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }) + ), + el('div', { className: 'grid-2', style: { alignItems: 'center' } }, + el('div', { style: { display: 'flex', flexDirection: 'column', gap: '1.5rem' } }, + el(IB, { + allowedBlocks: ['oribi/trust-item'], + template: [['oribi/trust-item', {}]], + templateLock: false + }) + ), + el('div', { style: { textAlign: 'center' } }, + el('span', { className: 'btn btn-primary btn-lg' }, a.btnText || 'Button'), + a.btnSub ? el('p', { className: 'lead mt-2', style: { fontSize: '.9rem' } }, a.btnSub) : null + ) + ) + ) + ) + ); + }, + save: function () { return el(IB.Content); } +}); + +/* 10. FAQ SECTION ─────────────────────────────────────────────────────── */ +reg('oribi/faq-section', { + title: 'Oribi FAQ Section', + icon: 'editor-help', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + variant: { type: 'string', default: 'normal' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + lead: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Section Settings' }, + el(SC, { label: 'Background', value: a.variant, options: [ + { label: 'Normal', value: 'normal' }, { label: 'Alternate', value: 'alt' } + ], onChange: function(v){s({variant:v});} }), + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: a.variant === 'alt' ? 'section section-alt' : 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'section-header' }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'FAQ heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }) + ), + el('div', { className: 'faq-list' }, + el(IB, { + allowedBlocks: ['oribi/faq-item'], + template: [['oribi/faq-item', {}]], + templateLock: false + }) + ) + ) + ) + ); + }, + save: function () { return el(IB.Content); } +}); + +/* FAQ ITEM (child) ─────────────────────────────────────────────────────── */ +reg('oribi/faq-item', { + title: 'Oribi FAQ Item', + icon: 'editor-help', + category: 'oribi', + parent: ['oribi/faq-section'], + supports: { html: false, reusable: false }, + attributes: { + question: { type: 'string', default: '' }, + answer: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + return el('details', { className: 'faq-item', open: true }, + el('summary', { className: 'faq-question' }, + el(RT, { tagName: 'span', value: a.question, + onChange: function(v){s({question:v});}, placeholder: 'Question...' }) + ), + el('div', { className: 'faq-answer' }, + el(RT, { tagName: 'p', value: a.answer, + onChange: function(v){s({answer:v});}, placeholder: 'Answer...' }) + ) + ); + }, + save: function () { return null; } +}); + +/* 11. COMPARISON TABLE (standalone) ───────────────────────────────────── */ +reg('oribi/comparison-table', { + title: 'Oribi Comparison Table', + icon: 'editor-table', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + variant: { type: 'string', default: 'normal' }, + label: { type: 'string', default: '' }, + heading: { type: 'string', default: '' }, + lead: { type: 'string', default: '' }, + columns: { type: 'array', default: [] }, + rows: { type: 'array', default: [] }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var cols = a.columns || []; + var rows = a.rows || []; + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Table Settings' }, + el(SC, { label: 'Background', value: a.variant, options: [ + { label: 'Normal', value: 'normal' }, { label: 'Alternate', value: 'alt' } + ], onChange: function(v){s({variant:v});} }), + el(TC, { label: 'Label', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: a.variant === 'alt' ? 'section section-alt' : 'section' }, + el('div', { className: 'container' }, + el('div', { className: 'section-header' }, + a.label ? el('span', { className: 'section-label' }, a.label) : null, + el(RT, { tagName: 'h2', value: a.heading, onChange: function(v){s({heading:v});}, placeholder: 'Table heading...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.lead, onChange: function(v){s({lead:v});}, placeholder: 'Lead text...' }) + ), + el('div', { className: 'comparison-table-wrap' }, + el('table', { className: 'comparison-table' }, + el('thead', null, + el('tr', null, + el('th', { className: 'comparison-feature-col' }, 'Feature'), + cols.map(function(col, i) { return el('th', { key: i }, col); }) + ) + ), + el('tbody', null, + rows.map(function(row, i) { + if (row.group) { + return el('tr', { key: i, className: 'comparison-group-row' }, + el('td', { colSpan: cols.length + 1 }, row.group) + ); + } + return el('tr', { key: i }, + el('td', { className: 'comparison-feature-name' }, row.feature || ''), + (row.values || []).map(function(val, j) { + return el('td', { key: j, className: 'comparison-cell' }, + val === true ? '\u2713' : val === false ? '\u2014' : String(val) + ); + }) + ); + }) + ) + ) + ) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* ═══════════════════════════════════════════════════════════════════════ + TEMPLATE-PART HELPER BLOCKS + ═══════════════════════════════════════════════════════════════════════ */ + +/* ═══════════════════════════════════════════════════════════════════════ + ANIMATED HERO BLOCKS (OTS Signs) + ═══════════════════════════════════════════════════════════════════════ */ + +/* ANIMATED HERO ───────────────────────────────────────────────────────── */ +reg('oribi/hero-animated', { + title: 'Animated Hero', + icon: 'admin-site-alt3', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + label: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + highlightWord: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + primaryBtnText: { type: 'string', default: 'Get Started' }, + primaryBtnUrl: { type: 'string', default: '/contact' }, + secondaryBtnText: { type: 'string', default: '' }, + secondaryBtnUrl: { type: 'string', default: '' }, + stat1Value: { type: 'string', default: '' }, + stat1Label: { type: 'string', default: '' }, + stat2Value: { type: 'string', default: '' }, + stat2Label: { type: 'string', default: '' }, + stat3Value: { type: 'string', default: '' }, + stat3Label: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + // Build particle elements for editor preview + var particles = []; + for (var i = 1; i <= 12; i++) { + particles.push(el('div', { key: 'p' + i, className: 'hero-particle hero-particle--' + i })); + } + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Highlight' }, + el(TC, { label: 'Word to highlight in title', value: a.highlightWord, onChange: function(v){s({highlightWord:v});} }) + ), + el(PB, { title: 'Primary Button' }, + el(TC, { label: 'URL', value: a.primaryBtnUrl, onChange: function(v){s({primaryBtnUrl:v});} }) + ), + el(PB, { title: 'Secondary Button', initialOpen: false }, + el(TC, { label: 'URL', value: a.secondaryBtnUrl, onChange: function(v){s({secondaryBtnUrl:v});} }) + ) + ), + el('section', { className: 'hero hero-animated' }, + el('div', { className: 'hero-particles', 'aria-hidden': 'true' }, particles), + el('div', { className: 'hero-animated__glow' }), + el('div', { className: 'container hero-animated__inner' }, + el('div', { className: 'hero-animated__content' }, + el(RT, { tagName: 'span', className: 'hero-label', value: a.label, + onChange: function(v){s({label:v});}, placeholder: '\u25CF Label text', allowedFormats: [] }), + el(RT, { tagName: 'h1', className: 'hero-title', value: a.title, + onChange: function(v){s({title:v});}, placeholder: 'Hero title...' }), + el(RT, { tagName: 'p', className: 'hero-description', value: a.description, + onChange: function(v){s({description:v});}, placeholder: 'Description...' }), + el('div', { className: 'btn-group' }, + el(RT, { tagName: 'span', className: 'btn btn-primary btn-lg', value: a.primaryBtnText, + onChange: function(v){s({primaryBtnText:v});}, placeholder: 'Button', allowedFormats: [] }), + el(RT, { tagName: 'span', className: 'btn btn-ghost btn-lg', value: a.secondaryBtnText, + onChange: function(v){s({secondaryBtnText:v});}, placeholder: 'Secondary button', allowedFormats: [] }) + ), + el('div', { className: 'hero-stats hero-stats--three' }, + el('div', null, + el(RT, { tagName: 'div', className: 'hero-stat-value', value: a.stat1Value, + onChange: function(v){s({stat1Value:v});}, placeholder: '\u2014', allowedFormats: [] }), + el(RT, { tagName: 'div', className: 'hero-stat-label', value: a.stat1Label, + onChange: function(v){s({stat1Label:v});}, placeholder: 'Stat label', allowedFormats: [] }) + ), + el('div', null, + el(RT, { tagName: 'div', className: 'hero-stat-value', value: a.stat2Value, + onChange: function(v){s({stat2Value:v});}, placeholder: '\u2014', allowedFormats: [] }), + el(RT, { tagName: 'div', className: 'hero-stat-label', value: a.stat2Label, + onChange: function(v){s({stat2Label:v});}, placeholder: 'Stat label', allowedFormats: [] }) + ), + el('div', null, + el(RT, { tagName: 'div', className: 'hero-stat-value', value: a.stat3Value, + onChange: function(v){s({stat3Value:v});}, placeholder: '\u2014', allowedFormats: [] }), + el(RT, { tagName: 'div', className: 'hero-stat-label', value: a.stat3Label, + onChange: function(v){s({stat3Label:v});}, placeholder: 'Stat label', allowedFormats: [] }) + ) + ) + ) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* ANIMATED PAGE HERO ──────────────────────────────────────────────────── */ +reg('oribi/page-hero-animated', { + title: 'Animated Page Hero', + icon: 'flag', + category: 'oribi', + supports: { align: ['full'], html: false }, + attributes: { + align: { type: 'string', default: 'full' }, + label: { type: 'string', default: '' }, + title: { type: 'string', default: '' }, + description: { type: 'string', default: '' }, + }, + edit: function (props) { + var a = props.attributes, s = props.setAttributes; + var particles = []; + for (var i = 1; i <= 8; i++) { + particles.push(el('div', { key: 'p' + i, className: 'hero-particle hero-particle--' + i })); + } + return el(Frag, null, + el(IC, null, + el(PB, { title: 'Settings' }, + el(TC, { label: 'Label (optional)', value: a.label, onChange: function(v){s({label:v});} }) + ) + ), + el('section', { className: 'page-hero page-hero-animated' }, + el('div', { className: 'hero-particles', 'aria-hidden': 'true' }, particles), + el('div', { className: 'hero-animated__glow' }), + el('div', { className: 'hero-overlay' }), + el('div', { className: 'container' }, + a.label ? el('span', { className: 'hero-label' }, a.label) : null, + el(RT, { tagName: 'h1', value: a.title, onChange: function(v){s({title:v});}, placeholder: 'Page title...' }), + el(RT, { tagName: 'p', className: 'lead', value: a.description, onChange: function(v){s({description:v});}, placeholder: 'Description...' }) + ) + ) + ); + }, + save: function () { return null; } +}); + +/* ═══════════════════════════════════════════════════════════════════════ + TEMPLATE-PART HELPER BLOCKS + ═══════════════════════════════════════════════════════════════════════ */ + +reg('oribi/site-header', { + title: 'Oribi Site Header', + icon: 'admin-home', + category: 'oribi', + supports: { html: false, multiple: false, reusable: false }, + edit: function () { + return el('div', { + style: { background: '#0D1321', color: '#fff', padding: '20px 24px', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' } + }, + el('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, + el('strong', { style: { fontSize: '1.2rem' } }, 'Oribi'), + el('span', { style: { fontSize: '1.2rem', fontWeight: 300 } }, 'Tech') + ), + el('div', { style: { display: 'flex', gap: '1.5rem', fontSize: '.9rem', opacity: 0.7 } }, + el('span', null, 'Services'), + el('span', null, 'About'), + el('span', null, 'FAQ'), + el('span', null, 'Contact') + ) + ); + }, + save: function () { return null; } +}); + +reg('oribi/site-footer', { + title: 'Oribi Site Footer', + icon: 'admin-home', + category: 'oribi', + supports: { html: false, multiple: false, reusable: false }, + edit: function () { + return el('div', { + style: { background: '#0D1321', color: '#fff', padding: '24px', borderRadius: '8px', textAlign: 'center' } + }, + el('strong', { style: { fontSize: '1.1rem' } }, 'OTS Theme β€” Site Footer'), + el('p', { style: { opacity: 0.5, margin: '8px 0 0', fontSize: '.85rem' } }, 'Brand Β· Service Links Β· Company Links Β· Connect Β· Copyright') + ); + }, + save: function () { return null; } +}); + +})(window.wp); diff --git a/theme/blocks/index.php b/theme/blocks/index.php new file mode 100644 index 0000000..2160c0d --- /dev/null +++ b/theme/blocks/index.php @@ -0,0 +1,1548 @@ + [ 'type' => 'number', 'default' => 0 ], + 'imgUrl' => [ 'type' => 'string', 'default' => '' ], + 'imgAlt' => [ 'type' => 'string', 'default' => '' ], + 'imgWidth' => [ 'type' => 'number', 'default' => 80 ], + 'imgHeight' => [ 'type' => 'number', 'default' => 0 ], + 'imgFit' => [ 'type' => 'string', 'default' => 'contain' ], + 'imgPosition' => [ 'type' => 'string', 'default' => 'top' ], + ]; +} + +/** + * Build card image HTML from shared image attributes. + * + * @param array $a Block attributes (must include imgId, imgUrl, etc.) + * @return array [ 'html' => string, 'position' => string, 'card_class' => string ] + */ +function oribi_card_image_html( $a ) { + $img_id = ! empty( $a['imgId'] ) ? intval( $a['imgId'] ) : 0; + $img_url = ! empty( $a['imgUrl'] ) ? $a['imgUrl'] : ''; + $img_alt = ! empty( $a['imgAlt'] ) ? $a['imgAlt'] : ''; + $img_w = ! empty( $a['imgWidth'] ) ? intval( $a['imgWidth'] ) : 80; + $img_h = ! empty( $a['imgHeight'] ) ? intval( $a['imgHeight'] ) : 0; + $img_fit = ! empty( $a['imgFit'] ) ? $a['imgFit'] : 'contain'; + $img_pos = ! empty( $a['imgPosition'] ) ? $a['imgPosition'] : 'top'; + + $result = [ + 'html' => '', + 'position' => $img_pos, + 'card_class' => '', + ]; + + if ( ! $img_url ) { + return $result; + } + + $result['card_class'] = 'img-' . $img_pos; + + // Build inline style for dimensions + $styles = []; + if ( $img_pos !== 'background' ) { + if ( $img_w > 0 ) $styles[] = 'width:' . $img_w . 'px'; + $styles[] = 'max-width:100%'; + if ( $img_h > 0 ) { + $styles[] = 'height:' . $img_h . 'px'; + } else { + $styles[] = 'height:auto'; + } + } + $style_str = implode( ';', $styles ); + + $fit_class = 'oribi-card-img oribi-card-img--' . ( $img_pos === 'background' ? 'cover' : esc_attr( $img_fit ) ); + + if ( $img_id ) { + $result['html'] = wp_get_attachment_image( $img_id, 'large', false, [ + 'class' => $fit_class, + 'style' => $style_str, + 'alt' => $img_alt, + ] ); + } else { + $result['html'] = '' . esc_attr( $img_alt )
+                        . ''; + } + + // Wrap in container + $result['html'] = '
' . $result['html'] . '
'; + + return $result; +} + +/** + * Shared icon attributes added to every card block that supports an icon. + */ +function oribi_card_icon_attributes() { + return [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + ]; +} + +/** + * Render an icon from block attributes. + * + * Accepts either: + * - iconType = 'fontawesome' + faIcon = 'fas fa-cloud' β†’ + * - iconType = 'emoji' + icon = '☁️' β†’ ☁️ (escaped) + * + * Legacy: if $icon_or_attrs is a plain string it behaves like before. + * + * @param array|string $icon_or_attrs Block attributes array, or legacy icon string. + * @return string Rendered HTML. + */ +function oribi_render_icon( $icon_or_attrs ) { + // Legacy string call + if ( is_string( $icon_or_attrs ) ) { + $icon = $icon_or_attrs; + if ( empty( $icon ) ) return ''; + if ( preg_match( '/^fa[srldb]\s+fa-/', $icon ) ) { + return ''; + } + return wp_kses_post( $icon ); + } + + // Array (block attributes) + $a = $icon_or_attrs; + $iconType = ! empty( $a['iconType'] ) ? $a['iconType'] : 'emoji'; + + if ( $iconType === 'fontawesome' ) { + $fa = ! empty( $a['faIcon'] ) ? trim( $a['faIcon'] ) : ''; + if ( empty( $fa ) ) return ''; + return ''; + } + + // emoji / text + $icon = ! empty( $a['icon'] ) ? $a['icon'] : ''; + if ( empty( $icon ) ) return ''; + return wp_kses_post( $icon ); +} + +/** + * Return true when the given block attributes specify a non-empty icon. + */ +function oribi_has_icon( $a ) { + $iconType = ! empty( $a['iconType'] ) ? $a['iconType'] : 'emoji'; + if ( $iconType === 'fontawesome' ) { + return ! empty( $a['faIcon'] ); + } + return ! empty( $a['icon'] ); +} + +/** + * Render a standard card section wrapper. + * + * @param array $a Block attributes (variant, label, heading, lead, columns). + * @param string $content InnerBlocks rendered HTML. + * @param string $grid_class CSS class for the grid container (e.g. 'grid'). + * @param int $default_cols Default column count if not set in attributes. + * @return string Rendered HTML. + */ +function oribi_render_card_section( $a, $content, $grid_class = 'grid', $default_cols = 3 ) { + $cls = ( ! empty( $a['variant'] ) && $a['variant'] === 'alt' ) ? 'section section-alt' : 'section'; + $cols = ! empty( $a['columns'] ) ? intval( $a['columns'] ) : $default_cols; + $grid = $grid_class . '-' . $cols; + ob_start(); ?> +
+
+
+ +

+

+
+
+ +
+
+
+ [ 'type' => 'string', 'default' => 'normal' ], + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + 'columns' => [ 'type' => 'number', 'default' => $default_cols ], + ]; +} + +/* ── Block category ────────────────────────────────────────────────────────── */ +add_filter( 'block_categories_all', function ( $cats ) { + array_unshift( $cats, [ 'slug' => 'oribi', 'title' => 'OTS Theme' ] ); + return $cats; +} ); + +/* ── Enqueue editor assets ─────────────────────────────────────────────────── */ +add_action( 'enqueue_block_editor_assets', function () { + $dir = get_template_directory(); + $uri = get_template_directory_uri(); + wp_enqueue_script( + 'oribi-blocks', + $uri . '/blocks/editor.js', + [ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ], + filemtime( $dir . '/blocks/editor.js' ), + true + ); + wp_enqueue_style( + 'oribi-blocks-editor', + $uri . '/blocks/editor.css', + [ 'wp-edit-blocks' ], + filemtime( $dir . '/blocks/editor.css' ) + ); +} ); + +/* ── Register all blocks ───────────────────────────────────────────────────── */ +add_action( 'init', function () { + + /* Shared supports β€” exposes color pickers and font-size selector in the + block inspector for every Oribi block. Individual blocks can override + these by merging their own array if needed. */ + $block_supports = [ + 'color' => [ + 'text' => true, + 'background' => true, + 'link' => true, + ], + 'typography' => [ + 'fontSize' => true, + 'lineHeight' => true, + ], + 'spacing' => [ + 'padding' => true, + 'margin' => true, + ], + ]; + + /* ── TEMPLATE-PART HELPER BLOCKS ──────────────────────────────────────── */ + + register_block_type( 'oribi/site-header', [ + 'render_callback' => 'oribi_render_site_header', + ] ); + + register_block_type( 'oribi/site-footer', [ + 'render_callback' => 'oribi_render_site_footer', + ] ); + + /* ── STANDALONE BLOCKS ─────────────────────────────────────────────────── */ + + register_block_type( 'oribi/hero', [ + 'attributes' => [ + 'label' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'highlightWord' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'primaryBtnText' => [ 'type' => 'string', 'default' => 'Get in Touch' ], + 'primaryBtnUrl' => [ 'type' => 'string', 'default' => '/contact' ], + 'secondaryBtnText' => [ 'type' => 'string', 'default' => '' ], + 'secondaryBtnUrl' => [ 'type' => 'string', 'default' => '' ], + 'stat1Value' => [ 'type' => 'string', 'default' => '' ], + 'stat1Label' => [ 'type' => 'string', 'default' => '' ], + 'stat2Value' => [ 'type' => 'string', 'default' => '' ], + 'stat2Label' => [ 'type' => 'string', 'default' => '' ], + 'svcLaptop1' => [ 'type' => 'string', 'default' => 'Data Backup' ], + 'svcLaptop2' => [ 'type' => 'string', 'default' => 'Endpoint Security' ], + 'svcLaptop3' => [ 'type' => 'string', 'default' => 'Patch Management' ], + 'svcCloud1' => [ 'type' => 'string', 'default' => 'Email Protection' ], + 'svcCloud2' => [ 'type' => 'string', 'default' => 'License Management' ], + 'svcCloud3' => [ 'type' => 'string', 'default' => 'Cloud Backup' ], + 'svcDesktop1' => [ 'type' => 'string', 'default' => 'Network Monitoring' ], + 'svcDesktop2' => [ 'type' => 'string', 'default' => 'Threat Detection' ], + 'svcDesktop3' => [ 'type' => 'string', 'default' => 'Cloud Management' ], + 'svcPhone1' => [ 'type' => 'string', 'default' => 'Mobile Security' ], + 'svcPhone2' => [ 'type' => 'string', 'default' => 'Data Encryption' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_hero', + ] ); + + register_block_type( 'oribi/page-hero', [ + 'attributes' => [ + 'label' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_page_hero', + ] ); + + register_block_type( 'oribi/cta-banner', [ + 'attributes' => [ + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'text' => [ 'type' => 'string', 'default' => '' ], + 'btnText' => [ 'type' => 'string', 'default' => '' ], + 'btnUrl' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_cta_banner', + ] ); + + register_block_type( 'oribi/intro-section', [ + 'attributes' => [ + 'variant' => [ 'type' => 'string', 'default' => 'normal' ], + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'visual' => [ 'type' => 'string', 'default' => '' ], + 'reversed' => [ 'type' => 'boolean', 'default' => false ], + 'imgId' => [ 'type' => 'number', 'default' => 0 ], + 'imgUrl' => [ 'type' => 'string', 'default' => '' ], + 'imgAlt' => [ 'type' => 'string', 'default' => '' ], + 'imgWidth' => [ 'type' => 'number', 'default' => 280 ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_intro_section', + ] ); + + register_block_type( 'oribi/contact-section', [ + 'attributes' => [ + 'heading' => [ 'type' => 'string', 'default' => "Let's Talk" ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + 'email' => [ 'type' => 'string', 'default' => 'solutions@oribi-tech.com' ], + 'supportUrl' => [ 'type' => 'string', 'default' => 'https://portal.oribi-tech.com/helpdesk/technical-support-1' ], + 'portalUrl' => [ 'type' => 'string', 'default' => 'https://portal.oribi-tech.com/' ], + 'location' => [ 'type' => 'string', 'default' => 'Saratoga Springs, Upstate New York' ], + 'formHeading' => [ 'type' => 'string', 'default' => 'Want to Learn More?' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_contact_section', + ] ); + + /* ── PARENT / CHILD PAIRS ──────────────────────────────────────────────── */ + + /* Feature Section (parent) */ + register_block_type( 'oribi/feature-section', [ + 'attributes' => oribi_card_section_attributes( 3 ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_feature_section', + ] ); + + /* Feature Card (child) */ + register_block_type( 'oribi/feature-card', [ + 'attributes' => array_merge( + [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'url' => [ 'type' => 'string', 'default' => '' ], + 'centered' => [ 'type' => 'boolean', 'default' => false ], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_feature_card', + ] ); + + /* Value Section (parent) */ + register_block_type( 'oribi/value-section', [ + 'attributes' => oribi_card_section_attributes( 3 ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_value_section', + ] ); + + /* Value Card (child) */ + register_block_type( 'oribi/value-card', [ + 'attributes' => array_merge( + [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_value_card', + ] ); + + /* Addon Section (parent) */ + register_block_type( 'oribi/addon-section', [ + 'attributes' => oribi_card_section_attributes( 3 ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_addon_section', + ] ); + + /* Addon Card (child) */ + register_block_type( 'oribi/addon-card', [ + 'attributes' => array_merge( + [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'tag' => [ 'type' => 'string', 'default' => '' ], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_addon_card', + ] ); + + /* Image Section (parent) */ + register_block_type( 'oribi/image-section', [ + 'attributes' => oribi_card_section_attributes( 3 ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_image_section', + ] ); + + /* Image Card (child) */ + register_block_type( 'oribi/image-card', [ + 'attributes' => array_merge( + [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'url' => [ 'type' => 'string', 'default' => '' ], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_image_card', + ] ); + + /* Stat Section (parent) */ + register_block_type( 'oribi/stat-section', [ + 'attributes' => oribi_card_section_attributes( 3 ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_stat_section', + ] ); + + /* Stat Card (child) */ + register_block_type( 'oribi/stat-card', [ + 'attributes' => array_merge( + [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'value' => [ 'type' => 'string', 'default' => '' ], + 'label' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_stat_card', + ] ); + + /* Link Section (parent) */ + register_block_type( 'oribi/link-section', [ + 'attributes' => oribi_card_section_attributes( 3 ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_link_section', + ] ); + + /* Link Card (child) */ + register_block_type( 'oribi/link-card', [ + 'attributes' => array_merge( + [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'linkText' => [ 'type' => 'string', 'default' => '' ], + 'linkUrl' => [ 'type' => 'string', 'default' => '' ], + ], + oribi_card_image_attributes() + ), + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_link_card', + ] ); + + /* Pricing Section (parent) */ + register_block_type( 'oribi/pricing-section', [ + 'attributes' => [ + 'variant' => [ 'type' => 'string', 'default' => 'normal' ], + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_pricing_section', + ] ); + + /* Pricing Card (child) */ + register_block_type( 'oribi/pricing-card', [ + 'attributes' => [ + 'icon' => [ 'type' => 'string', 'default' => '' ], + 'iconType' => [ 'type' => 'string', 'default' => 'emoji' ], + 'faIcon' => [ 'type' => 'string', 'default' => '' ], + 'name' => [ 'type' => 'string', 'default' => '' ], + 'tagline' => [ 'type' => 'string', 'default' => '' ], + 'features' => [ 'type' => 'array', 'default' => [] ], + 'btnText' => [ 'type' => 'string', 'default' => 'Get Started' ], + 'btnUrl' => [ 'type' => 'string', 'default' => '/contact' ], + 'featured' => [ 'type' => 'boolean', 'default' => false ], + 'badge' => [ 'type' => 'string', 'default' => '' ], + 'imgId' => [ 'type' => 'number', 'default' => 0 ], + 'imgUrl' => [ 'type' => 'string', 'default' => '' ], + 'imgAlt' => [ 'type' => 'string', 'default' => '' ], + 'imgWidth' => [ 'type' => 'number', 'default' => 80 ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_pricing_card', + ] ); + + /* Platform Section (parent) */ + register_block_type( 'oribi/platform-section', [ + 'attributes' => [ + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_platform_section', + ] ); + + /* Platform Row (child) */ + register_block_type( 'oribi/platform-row', [ + 'attributes' => [ + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'btnText' => [ 'type' => 'string', 'default' => 'Learn More' ], + 'btnUrl' => [ 'type' => 'string', 'default' => '' ], + 'visual' => [ 'type' => 'string', 'default' => '' ], + 'reversed' => [ 'type' => 'boolean', 'default' => false ], + 'imgId' => [ 'type' => 'number', 'default' => 0 ], + 'imgUrl' => [ 'type' => 'string', 'default' => '' ], + 'imgAlt' => [ 'type' => 'string', 'default' => '' ], + 'imgWidth' => [ 'type' => 'number', 'default' => 300 ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_platform_row', + ] ); + + /* FAQ Section (parent) */ + register_block_type( 'oribi/faq-section', [ + 'attributes' => [ + 'variant' => [ 'type' => 'string', 'default' => 'normal' ], + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_faq_section', + ] ); + + /* FAQ Item (child) */ + register_block_type( 'oribi/faq-item', [ + 'attributes' => [ + 'question' => [ 'type' => 'string', 'default' => '' ], + 'answer' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_faq_item', + ] ); + + /* Comparison Table */ + register_block_type( 'oribi/comparison-table', [ + 'attributes' => [ + 'variant' => [ 'type' => 'string', 'default' => 'normal' ], + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + 'columns' => [ 'type' => 'array', 'default' => [] ], + 'rows' => [ 'type' => 'array', 'default' => [] ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_comparison_table', + ] ); + + /* Trust Section (parent) */ + register_block_type( 'oribi/trust-section', [ + 'attributes' => [ + 'label' => [ 'type' => 'string', 'default' => '' ], + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'lead' => [ 'type' => 'string', 'default' => '' ], + 'btnText' => [ 'type' => 'string', 'default' => '' ], + 'btnUrl' => [ 'type' => 'string', 'default' => '' ], + 'btnSub' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_trust_section', + ] ); + + /* Trust Item (child) */ + register_block_type( 'oribi/trust-item', [ + 'attributes' => [ + 'heading' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_trust_item', + ] ); + + /* ── ANIMATED HERO BLOCKS (OTS Signs) ─────────────────────────────────── */ + + /* Animated Hero β€” full homepage hero with particle background */ + register_block_type( 'oribi/hero-animated', [ + 'attributes' => [ + 'label' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'highlightWord' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + 'primaryBtnText' => [ 'type' => 'string', 'default' => 'Get Started' ], + 'primaryBtnUrl' => [ 'type' => 'string', 'default' => '/contact' ], + 'secondaryBtnText' => [ 'type' => 'string', 'default' => '' ], + 'secondaryBtnUrl' => [ 'type' => 'string', 'default' => '' ], + 'stat1Value' => [ 'type' => 'string', 'default' => '' ], + 'stat1Label' => [ 'type' => 'string', 'default' => '' ], + 'stat2Value' => [ 'type' => 'string', 'default' => '' ], + 'stat2Label' => [ 'type' => 'string', 'default' => '' ], + 'stat3Value' => [ 'type' => 'string', 'default' => '' ], + 'stat3Label' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_hero_animated', + ] ); + + /* Animated Page Hero β€” inner pages with particle background */ + register_block_type( 'oribi/page-hero-animated', [ + 'attributes' => [ + 'label' => [ 'type' => 'string', 'default' => '' ], + 'title' => [ 'type' => 'string', 'default' => '' ], + 'description' => [ 'type' => 'string', 'default' => '' ], + ], + 'supports' => $block_supports, + 'render_callback' => 'oribi_render_page_hero_animated', + ] ); + +} ); + +/* ══════════════════════════════════════════════════════════════════════════════ + RENDER CALLBACKS + ══════════════════════════════════════════════════════════════════════════════ */ + +/* ── Site Header ───────────────────────────────────────────────────────────── */ +function oribi_render_site_header() { + $has_logo = has_custom_logo(); + ob_start(); ?> + +'; + echo '
  • Services
  • '; + echo '
  • About
  • '; + echo '
  • FAQ
  • '; + echo '
  • Contact
  • '; + echo ''; +} + +/* ── Site Footer ───────────────────────────────────────────────────────────── */ +function oribi_render_site_footer() { + $year = gmdate( 'Y' ); + ob_start(); ?> + + +' . esc_html( $word ) . '', + wp_kses_post( $text ) + ); +} + +/* ── Hero ──────────────────────────────────────────────────────────────────── */ +function oribi_render_hero( $a ) { + ob_start(); + ?> +
    +
    +
    + +

    +

    +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
    • +
    • +
    +
    + + +
    +
    +
    + + 365 +
    +
    +
      +
    • +
    • +
    • +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
    • +
    • +
    +
    + + +
    +
    +
    +
    +
    + βœ“ + Secure +
    +
    +
    +
    +
      +
    • +
    • +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    + +

    +

    +
    +
    + +
    +
    +

    +

    + +
    +
    + +
    +
    +
    > + > + +

    +

    +
    +
    >
    +
    + +
    + +
    +
    +
    +
    +

    +

    +
    📧
    Email Us
    +
    🎫
    Existing Customer Support
    +
    🌎
    Client Portal
    +
    📍
    Location
    +
    +
    +

    +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    + +< class="oribi-card"> + +
    +

    +

    +
    +> + +< class="oribi-card"> + +
    +
    >
    +

    +

    +
    +> + +< class="oribi-card"> + + + + +
    >
    + +

    +

    +> + +
    + +
    +
    +

    +

    +
    +
    + +
    + +
    +

    +

    +
    +
    + +
    + + +
    + +

    +

    +
    + +
    + +
    + +

    +

    +
    +
    + +
    + +
    +
    + +

    +

    +
    +
    + +
    + + +
    + + +

    +

    +
    + +< class="oribi-card image-card"> + +
    +

    +

    +
    +> + +< class="oribi-card image-card"> + +
    +
    +

    +

    +
    +> + +< class="oribi-card image-card"> + + +
    + +
    +

    +

    +
    +> + +
    + +
    +
    +
    +
    +

    +
    +
    + +
    + +
    +
    +
    +

    +
    +
    + +
    + +
    +
    +
    +

    +
    +' + . esc_html( $a['linkText'] ?? 'Learn More' ) . ''; + } + + ob_start(); + if ( $img['html'] && $img['position'] === 'left' ) : ?> + + + + + +inner_blocks ); + ob_start(); ?> +
    +
    +
    + +

    +

    +
    +
    + +
    +
    +
    + 'card-image', + 'style' => $img_style, + 'alt' => $img_alt, + ] ); + } else { + $img_html = '' . esc_attr( $img_alt ) . ''; + } + } + + ob_start(); ?> +
    + + + + +
    + +
    +
    +

    +
      + +
    • + +
    + +
    + +
    +
    +
    + +

    +

    +
    + +
    +
    + $img_style, 'alt' => $img_alt ] ); + } else { + $visual_html = '' . esc_attr( $img_alt ) . ''; + } + $visual_cls = 'platform-visual has-img'; + } else { + $visual_html = oribi_render_icon( $a['visual'] ?? '' ); + $visual_cls = 'platform-visual'; + } + + ob_start(); ?> +
    +
    +

    +

    + + + +
    +
    +
    + +
    +
    +
    + +

    +

    +
    +
    +
    + +
    +
    + +

    +
    +
    +
    +
    + +
    +

    +

    +
    + +
    +
    +
    + +

    +

    +
    +
    + +
    +
    +
    + +
    + +

    +
    + +
    +
    +
    + +

    +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Feature
    ✓'; + elseif ( $val === false ) echo ''; + else echo wp_kses_post( $val ); + ?>
    +
    +
    +
    +'; + for ( $i = 1; $i <= $count; $i++ ) { + $html .= '
    '; + } + $html .= ''; + return $html; +} + +/* ── Animated Hero (homepage) ──────────────────────────────────────────────── */ +function oribi_render_hero_animated( $a ) { + ob_start(); + ?> +
    + +
    +
    +
    + + + +

    +

    +
    + + + + + + +
    + +
    + +
    + + +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +

    +

    +
    +
    +get( 'Version' ) ); +define( 'ORIBI_DIR', get_template_directory() ); +define( 'ORIBI_URI', get_template_directory_uri() ); + +/* ── Includes ──────────────────────────────────────────────── */ +require_once ORIBI_DIR . '/inc/setup.php'; +require_once ORIBI_DIR . '/inc/enqueue.php'; +require_once ORIBI_DIR . '/inc/ajax.php'; + +/* ── Customisable design-token system ──────────────────────── */ +require_once ORIBI_DIR . '/inc/theme-defaults.php'; +require_once ORIBI_DIR . '/inc/font-manager.php'; +require_once ORIBI_DIR . '/inc/theme-generator.php'; +require_once ORIBI_DIR . '/inc/theme-settings.php'; + +/* ── Custom Blocks (render callbacks + editor assets) ──────── */ +require_once ORIBI_DIR . '/blocks/index.php'; diff --git a/theme/inc/ajax.php b/theme/inc/ajax.php new file mode 100644 index 0000000..8b823e8 --- /dev/null +++ b/theme/inc/ajax.php @@ -0,0 +1,64 @@ +', $name, $email ), + ]; + + $sent = wp_mail( $to, $subject, $body, $headers ); + + if ( $sent ) { + wp_send_json_success( "Thanks! We'll get back to you shortly." ); + } else { + wp_send_json_error( 'Something went wrong. Please try again or email us directly.' ); + } +} diff --git a/theme/inc/enqueue.php b/theme/inc/enqueue.php new file mode 100644 index 0000000..273f9d4 --- /dev/null +++ b/theme/inc/enqueue.php @@ -0,0 +1,110 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'oribi_contact_nonce' ), + ] ); + + // Threaded comments (if ever enabled) + if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) { + wp_enqueue_script( 'comment-reply' ); + } +} ); + +/* ── Font Awesome 6 Free (CDN) ─────────────────────────────── */ +add_action( 'wp_enqueue_scripts', function () { + wp_enqueue_style( + 'font-awesome', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css', + [], + null + ); +} ); + +add_action( 'enqueue_block_editor_assets', function () { + wp_enqueue_style( + 'font-awesome', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css', + [], + null + ); +} ); + +/* ── Google Fonts β€” dynamic based on theme settings ────────── */ +add_action( 'wp_enqueue_scripts', 'oribi_enqueue_selected_fonts' ); +add_action( 'enqueue_block_editor_assets', 'oribi_enqueue_selected_fonts' ); + +/** + * Enqueue Google Fonts stylesheets for the selected body & heading fonts. + */ +function oribi_enqueue_selected_fonts() { + if ( ! function_exists( 'oribi_get_setting' ) ) { + return; + } + + $slugs = array_unique( array_filter( [ + oribi_get_setting( 'font_family' ), + oribi_get_setting( 'font_heading' ), + ] ) ); + + foreach ( $slugs as $slug ) { + $url = oribi_get_google_font_url( $slug ); + if ( $url ) { + wp_enqueue_style( + 'oribi-gf-' . $slug, + $url, + [], + null + ); + } + } +} + +/* ── Generated theme CSS (colour / font / spacing overrides) ─ */ +add_action( 'wp_enqueue_scripts', 'oribi_enqueue_generated_css', 20 ); +add_action( 'enqueue_block_editor_assets', 'oribi_enqueue_generated_css', 20 ); + +/** + * Enqueue the dynamically generated CSS file that applies design-token + * overrides from the admin settings page. + */ +function oribi_enqueue_generated_css() { + if ( ! function_exists( 'oribi_generated_css_url' ) ) { + return; + } + + $url = oribi_generated_css_url(); + $ver = get_theme_mod( 'oribi_css_version', '1' ); + + wp_enqueue_style( 'oribi-custom-tokens', $url, [ 'oribi-main' ], $ver ); +} diff --git a/theme/inc/font-manager.php b/theme/inc/font-manager.php new file mode 100644 index 0000000..2bbd99d --- /dev/null +++ b/theme/inc/font-manager.php @@ -0,0 +1,260 @@ + + */ +function oribi_get_available_fonts() { + + $fonts = []; + + /* ── 1. theme.json registered families ─────────────────── */ + $theme_json = WP_Theme_JSON_Resolver::get_merged_data()->get_data(); + if ( ! empty( $theme_json['settings']['typography']['fontFamilies']['theme'] ) ) { + foreach ( $theme_json['settings']['typography']['fontFamilies']['theme'] as $f ) { + $fonts[ $f['slug'] ] = [ + 'slug' => $f['slug'], + 'name' => $f['name'] ?? $f['slug'], + 'fontFamily' => $f['fontFamily'], + ]; + } + } + + /* ── 2. Font Library (user-installed via Site Editor) ───── */ + $font_families = oribi_query_font_library(); + foreach ( $font_families as $f ) { + $slug = sanitize_title( $f['slug'] ?? $f['name'] ); + if ( ! isset( $fonts[ $slug ] ) ) { + $fonts[ $slug ] = [ + 'slug' => $slug, + 'name' => $f['name'], + 'fontFamily' => $f['fontFamily'] ?? "'{$f['name']}', sans-serif", + ]; + } + } + + /* ── 3. Bundled Google Fonts (always shown as available) ── */ + $bundled = oribi_get_bundled_google_fonts(); + foreach ( $bundled as $slug => $data ) { + if ( ! isset( $fonts[ $slug ] ) ) { + $fonts[ $slug ] = $data; + } + } + + // Sort alphabetically by display name. + uasort( $fonts, function ( $a, $b ) { + return strcasecmp( $a['name'], $b['name'] ); + } ); + + return array_values( $fonts ); +} + +/** + * Query the WP Font Library (wp_font_family post type) for installed fonts. + * + * @return array + */ +function oribi_query_font_library() { + + $results = []; + + // Font Library stores each font family as a wp_font_family post (WP 6.5+). + $query = new WP_Query( [ + 'post_type' => 'wp_font_family', + 'posts_per_page' => 100, + 'post_status' => 'publish', + 'no_found_rows' => true, + ] ); + + if ( $query->have_posts() ) { + foreach ( $query->posts as $post ) { + $content = json_decode( $post->post_content, true ); + if ( $content ) { + $results[] = [ + 'slug' => $content['slug'] ?? sanitize_title( $post->post_title ), + 'name' => $post->post_title, + 'fontFamily' => $content['fontFamily'] ?? "'{$post->post_title}', sans-serif", + ]; + } + } + } + wp_reset_postdata(); + + return $results; +} + +/** + * Return a curated set of popular Google Fonts bundled with the theme. + * + * These are always shown in the font selector even if the user hasn't + * explicitly installed them via the Font Library. WordPress will load + * them from Google Fonts CDN when selected. + * + * @return array + */ +function oribi_get_bundled_google_fonts() { + + return [ + 'inter' => [ + 'slug' => 'inter', + 'name' => 'Inter', + 'fontFamily' => "'Inter', system-ui, -apple-system, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap', + ], + 'roboto' => [ + 'slug' => 'roboto', + 'name' => 'Roboto', + 'fontFamily' => "'Roboto', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap', + ], + 'open-sans' => [ + 'slug' => 'open-sans', + 'name' => 'Open Sans', + 'fontFamily' => "'Open Sans', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap', + ], + 'poppins' => [ + 'slug' => 'poppins', + 'name' => 'Poppins', + 'fontFamily' => "'Poppins', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap', + ], + 'lato' => [ + 'slug' => 'lato', + 'name' => 'Lato', + 'fontFamily' => "'Lato', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap', + ], + 'montserrat' => [ + 'slug' => 'montserrat', + 'name' => 'Montserrat', + 'fontFamily' => "'Montserrat', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800;900&display=swap', + ], + 'source-sans-3' => [ + 'slug' => 'source-sans-3', + 'name' => 'Source Sans 3', + 'fontFamily' => "'Source Sans 3', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;500;600;700;800;900&display=swap', + ], + 'nunito' => [ + 'slug' => 'nunito', + 'name' => 'Nunito', + 'fontFamily' => "'Nunito', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700;800;900&display=swap', + ], + 'raleway' => [ + 'slug' => 'raleway', + 'name' => 'Raleway', + 'fontFamily' => "'Raleway', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;500;600;700;800;900&display=swap', + ], + 'dm-sans' => [ + 'slug' => 'dm-sans', + 'name' => 'DM Sans', + 'fontFamily' => "'DM Sans', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap', + ], + 'work-sans' => [ + 'slug' => 'work-sans', + 'name' => 'Work Sans', + 'fontFamily' => "'Work Sans', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;600;700;800;900&display=swap', + ], + 'plus-jakarta-sans' => [ + 'slug' => 'plus-jakarta-sans', + 'name' => 'Plus Jakarta Sans', + 'fontFamily' => "'Plus Jakarta Sans', system-ui, sans-serif", + 'googleUrl' => 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap', + ], + 'system-ui' => [ + 'slug' => 'system-ui', + 'name' => 'System UI (no download)', + 'fontFamily' => "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif", + 'googleUrl' => '', + ], + ]; +} + +/** + * Resolve a font slug to its CSS font-family value. + * + * Checks (in order): bundled list β†’ Font Library β†’ theme.json. + * + * @param string $slug Font slug (e.g. 'inter', 'roboto'). + * @return string CSS font-family value. + */ +function oribi_get_font_family_css( $slug ) { + + if ( empty( $slug ) ) { + $slug = 'inter'; + } + + // Bundled fonts (includes Google Fonts URL for enqueue). + $bundled = oribi_get_bundled_google_fonts(); + if ( isset( $bundled[ $slug ] ) ) { + return $bundled[ $slug ]['fontFamily']; + } + + // Search all available fonts. + $fonts = oribi_get_available_fonts(); + foreach ( $fonts as $f ) { + if ( $f['slug'] === $slug ) { + return $f['fontFamily']; + } + } + + // Ultimate fallback. + return "'Inter', system-ui, -apple-system, sans-serif"; +} + +/** + * Return the Google Fonts stylesheet URL for a given font slug, + * or empty string if not a bundled Google Font. + * + * @param string $slug Font slug. + * @return string URL or ''. + */ +function oribi_get_google_font_url( $slug ) { + $bundled = oribi_get_bundled_google_fonts(); + return isset( $bundled[ $slug ] ) ? $bundled[ $slug ]['googleUrl'] : ''; +} diff --git a/theme/inc/setup.php b/theme/inc/setup.php new file mode 100644 index 0000000..c7f252a --- /dev/null +++ b/theme/inc/setup.php @@ -0,0 +1,63 @@ + 40, + 'width' => 180, + 'flex-height' => true, + 'flex-width' => true, + ] ); + add_theme_support( 'html5', [ + 'search-form', + 'comment-form', + 'comment-list', + 'gallery', + 'caption', + 'style', + 'script', + ] ); + + // Load main.css inside the block editor so previews match the frontend + add_editor_style( 'assets/css/main.css' ); + + // Navigation menus (used by oribi/site-header block) + register_nav_menus( [ + 'primary' => __( 'Primary Menu', 'ots-theme' ), + 'footer' => __( 'Footer Menu', 'ots-theme' ), + ] ); +} ); + +/* ── Block pattern categories ──────────────────────────────── */ +add_action( 'init', function () { + register_block_pattern_category( 'oribi-pages', [ + 'label' => __( 'Oribi Tech β€” Pages', 'ots-theme' ), + ] ); + register_block_pattern_category( 'oribi-sections', [ + 'label' => __( 'Oribi Tech β€” Sections', 'ots-theme' ), + ] ); +} ); + +/* ── Remove core block patterns if desired ─────────────────── */ +add_action( 'after_setup_theme', function () { + remove_theme_support( 'core-block-patterns' ); +} ); diff --git a/theme/inc/theme-defaults.php b/theme/inc/theme-defaults.php new file mode 100644 index 0000000..d0ae898 --- /dev/null +++ b/theme/inc/theme-defaults.php @@ -0,0 +1,111 @@ + + */ +function oribi_get_theme_defaults() { + + return [ + + /* ── Light-mode colour palette ──────────────────────── */ + 'color_primary' => '#D83302', + 'color_primary_dk' => '#B52B02', + 'color_primary_lt' => '#FEF0EB', + 'color_accent' => '#00757c', + 'color_accent_dk' => '#005a60', + 'color_accent_lt' => '#E6F4F5', + 'color_dark' => '#0D1321', + 'color_dark_2' => '#1A2236', + 'color_text' => '#2D3748', + 'color_text_muted' => '#718096', + 'color_border' => '#E2E8F0', + 'color_bg' => '#FFFFFF', + 'color_bg_alt' => '#FFF8F5', + + /* ── Dark-mode colour palette ───────────────────────── */ + 'dark_primary' => '#FF6B3D', + 'dark_primary_dk' => '#D83302', + 'dark_primary_lt' => 'rgba(216,51,2,0.15)', + 'dark_accent' => '#00757c', + 'dark_accent_dk' => '#005a60', + 'dark_accent_lt' => 'rgba(0,117,124,0.15)', + 'dark_dark' => '#E2E8F0', + 'dark_dark_2' => '#CBD5E0', + 'dark_text' => '#CBD5E0', + 'dark_text_muted' => '#A0AEC0', + 'dark_border' => '#2D3748', + 'dark_bg' => '#0F1724', + 'dark_bg_alt' => '#151F30', + 'dark_bg_dark' => '#0A0F1A', + 'dark_heading' => '#F7FAFC', + 'dark_card_bg' => '#151F30', + + /* ── Typography ─────────────────────────────────────── */ + 'font_family' => 'inter', // slug from WP Font Library + 'font_heading' => '', // empty = same as body + + /* ── Border radii ───────────────────────────────────── */ + 'radius_sm' => '6', + 'radius_md' => '12', + 'radius_lg' => '20', + 'radius_xl' => '32', + + /* ── Spacing / layout ───────────────────────────────── */ + 'container_max' => '1200', + 'container_pad_min' => '1', + 'container_pad_max' => '2', + 'wide_size' => '1536', + ]; +} + +/** + * Retrieve a single design-token value, respecting saved theme-mod overrides. + * + * @param string $key Token name (e.g. 'color_primary'). + * @return string + */ +function oribi_get_setting( $key ) { + $defaults = oribi_get_theme_defaults(); + $default = isset( $defaults[ $key ] ) ? $defaults[ $key ] : ''; + return get_theme_mod( 'oribi_' . $key, $default ); +} + +/** + * Persist the current set of defaults into theme-mods (one-time migration). + * + * Called the first time the settings page is loaded, so that existing sites + * start with their current visual identity already stored. + */ +function oribi_maybe_seed_defaults() { + + if ( get_theme_mod( 'oribi_defaults_seeded' ) ) { + return; + } + + $defaults = oribi_get_theme_defaults(); + + foreach ( $defaults as $key => $value ) { + // Only set if the user hasn't already saved a value. + if ( false === get_theme_mod( 'oribi_' . $key, false ) ) { + set_theme_mod( 'oribi_' . $key, $value ); + } + } + + set_theme_mod( 'oribi_defaults_seeded', true ); +} diff --git a/theme/inc/theme-generator.php b/theme/inc/theme-generator.php new file mode 100644 index 0000000..f5a4aaa --- /dev/null +++ b/theme/inc/theme-generator.php @@ -0,0 +1,306 @@ + .wp-block-group, +.wp-site-blocks > .alignfull { + max-width: none; +} + +body > .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { + max-width: {$c_max}px; +} + +CSS; + + return sprintf( $css, gmdate( 'Y-m-d H:i:s' ) ); +} + +/** + * Write the generated CSS file to disk. + * + * @return bool|WP_Error True on success, WP_Error on failure. + */ +function oribi_write_generated_css() { + + $css = oribi_build_css(); + $path = oribi_generated_css_path(); + + // Ensure directory exists. + $dir = dirname( $path ); + if ( ! file_exists( $dir ) ) { + wp_mkdir_p( $dir ); + } + + // Use WP_Filesystem for safe file writing. + require_once ABSPATH . 'wp-admin/includes/file.php'; + global $wp_filesystem; + if ( ! WP_Filesystem() ) { + return new WP_Error( 'fs', __( 'Could not initialise filesystem.', 'ots-theme' ) ); + } + + $result = $wp_filesystem->put_contents( $path, $css, FS_CHMOD_FILE ); + if ( ! $result ) { + return new WP_Error( 'write', __( 'Could not write generated CSS file.', 'ots-theme' ) ); + } + + // Bump version number so browsers cache-bust. + $version = intval( get_theme_mod( 'oribi_css_version', 0 ) ) + 1; + set_theme_mod( 'oribi_css_version', $version ); + + return true; +} + +/** + * Regenerate the CSS file if it doesn't exist yet (e.g. first page load). + * + * Hooked early so the file is ready before wp_enqueue_scripts fires. + */ +add_action( 'init', function () { + if ( ! file_exists( oribi_generated_css_path() ) ) { + oribi_write_generated_css(); + } +} ); diff --git a/theme/inc/theme-settings.php b/theme/inc/theme-settings.php new file mode 100644 index 0000000..acd4817 --- /dev/null +++ b/theme/inc/theme-settings.php @@ -0,0 +1,732 @@ + $value ) { + set_theme_mod( 'oribi_' . $key, $value ); + } + } else { + // Sanitize & save each setting. + foreach ( $defaults as $key => $default ) { + $posted = isset( $_POST[ 'oribi_' . $key ] ) + ? wp_unslash( $_POST[ 'oribi_' . $key ] ) // phpcs:ignore + : $default; + + // Determine sanitisation method by key prefix/type. + if ( strpos( $key, 'color_' ) === 0 || strpos( $key, 'dark_' ) === 0 ) { + // Colour values (hex or rgba). + $posted = oribi_sanitize_color( $posted ); + } elseif ( strpos( $key, 'font_' ) === 0 ) { + $posted = sanitize_text_field( $posted ); + } elseif ( strpos( $key, 'radius_' ) === 0 || strpos( $key, 'container_' ) === 0 || $key === 'wide_size' ) { + $posted = sanitize_text_field( $posted ); + } else { + $posted = sanitize_text_field( $posted ); + } + + set_theme_mod( 'oribi_' . $key, $posted ); + } + } + + // Regenerate CSS. + $result = oribi_write_generated_css(); + if ( is_wp_error( $result ) ) { + add_settings_error( 'oribi_settings', 'css_error', $result->get_error_message(), 'error' ); + } else { + $msg = 'reset' === $action + ? __( 'Settings reset to defaults. CSS regenerated.', 'ots-theme' ) + : __( 'Settings saved. CSS regenerated.', 'ots-theme' ); + add_settings_error( 'oribi_settings', 'saved', $msg, 'success' ); + } + + // Store errors/notices in transient so they survive the redirect. + set_transient( 'oribi_settings_notices', get_settings_errors( 'oribi_settings' ), 30 ); + + // PRG redirect. + wp_safe_redirect( admin_url( 'themes.php?page=oribi-theme-settings' ) ); + exit; +} ); + +/** + * Sanitize a colour value (hex or rgba). + * + * @param string $value Raw colour value. + * @return string Sanitized colour. + */ +function oribi_sanitize_color( $value ) { + $value = trim( $value ); + + // Allow rgba(...) values (used for dark mode light tints). + if ( preg_match( '/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*[\d.]+\s*)?\)$/', $value ) ) { + return $value; + } + + // Standard hex. + return sanitize_hex_color( $value ) ?? '#000000'; +} + +/* ── Render the settings page ──────────────────────────────────── */ +function oribi_render_settings_page() { + + // Seed defaults on first visit. + oribi_maybe_seed_defaults(); + + // Show any saved notices. + $notices = get_transient( 'oribi_settings_notices' ); + if ( $notices ) { + delete_transient( 'oribi_settings_notices' ); + foreach ( $notices as $notice ) { + printf( + '

    %s

    ', + esc_attr( $notice['type'] ), + esc_html( $notice['message'] ) + ); + } + } + + $s = 'oribi_get_setting'; + $fonts = oribi_get_available_fonts(); + + ?> +
    +

    +

    + +

    + +
    + + + + + + +
    +
    + + +
    +

    +

    + +
    + __( 'Primary', 'ots-theme' ), + 'color_primary_dk' => __( 'Primary Dark', 'ots-theme' ), + 'color_primary_lt' => __( 'Primary Light', 'ots-theme' ), + 'color_accent' => __( 'Accent', 'ots-theme' ), + 'color_accent_dk' => __( 'Accent Dark', 'ots-theme' ), + 'color_accent_lt' => __( 'Accent Light', 'ots-theme' ), + 'color_dark' => __( 'Dark', 'ots-theme' ), + 'color_dark_2' => __( 'Dark 2', 'ots-theme' ), + 'color_text' => __( 'Text', 'ots-theme' ), + 'color_text_muted' => __( 'Text Muted', 'ots-theme' ), + 'color_border' => __( 'Border', 'ots-theme' ), + 'color_bg' => __( 'Background', 'ots-theme' ), + 'color_bg_alt' => __( 'Background Alt', 'ots-theme' ), + ]; + foreach ( $light_colors as $key => $label ) : + $val = $s( $key ); + ?> +
    + + +
    + +
    +
    + + +
    +

    +

    + +
    + __( 'Primary', 'ots-theme' ), + 'dark_primary_dk' => __( 'Primary Dark', 'ots-theme' ), + 'dark_primary_lt' => __( 'Primary Light', 'ots-theme' ), + 'dark_accent' => __( 'Accent', 'ots-theme' ), + 'dark_accent_dk' => __( 'Accent Dark', 'ots-theme' ), + 'dark_accent_lt' => __( 'Accent Light', 'ots-theme' ), + 'dark_dark' => __( 'Dark (Text)', 'ots-theme' ), + 'dark_dark_2' => __( 'Dark 2 (Text)', 'ots-theme' ), + 'dark_text' => __( 'Body Text', 'ots-theme' ), + 'dark_text_muted' => __( 'Text Muted', 'ots-theme' ), + 'dark_border' => __( 'Border', 'ots-theme' ), + 'dark_bg' => __( 'Background', 'ots-theme' ), + 'dark_bg_alt' => __( 'Background Alt', 'ots-theme' ), + 'dark_bg_dark' => __( 'Background Darker', 'ots-theme' ), + 'dark_heading' => __( 'Heading', 'ots-theme' ), + 'dark_card_bg' => __( 'Card Background', 'ots-theme' ), + ]; + foreach ( $dark_colors as $key => $label ) : + $val = $s( $key ); + $is_rgba = ( strpos( $val, 'rgba' ) !== false ); + ?> +
    + + + + + + + +
    + +
    +
    + + +
    +

    +

    + ' . esc_html__( 'Font Library', 'ots-theme' ) . '' + ); + ?> +

    + + + + + + + + + + + + + +
    +

    +
    + The quick brown fox jumps over the lazy dog +
    +
    + The quick brown fox jumps over the lazy dog. 0123456789 +
    +
    +
    + + +
    +

    + + + + + + + + + + + + + + + +
    + + +
    +

    +

    + + + [ 'SM', __( 'Buttons, badges, small elements', 'ots-theme' ) ], + 'radius_md' => [ 'MD', __( 'Cards, inputs, mid-size components', 'ots-theme' ) ], + 'radius_lg' => [ 'LG', __( 'Sections, larger surfaces', 'ots-theme' ) ], + 'radius_xl' => [ 'XL', __( 'Pills, fully rounded elements', 'ots-theme' ) ], + ]; + foreach ( $radii as $key => $info ) : + ?> + + + + + + +
    + +
    + + + +
    + + +
    + + +
    +
    +
    + diff --git a/theme/parts/header.html b/theme/parts/header.html new file mode 100644 index 0000000..798afcc --- /dev/null +++ b/theme/parts/header.html @@ -0,0 +1 @@ + diff --git a/theme/readme.txt b/theme/readme.txt new file mode 100644 index 0000000..7a2b1b6 --- /dev/null +++ b/theme/readme.txt @@ -0,0 +1,65 @@ +=== Oribi Tech === + +Contributors: Oribi Technology Services +Requires at least: 6.4 +Tested up to: 6.7 +Requires PHP: 7.4 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +== Description == + +Oribi Tech is a Full Site Editing (FSE) block theme built for Oribi Technology Services. It features 15+ custom Gutenberg blocks, dark/light mode toggle, a canvas-based datacenter hero animation, and complete page patterns for every service page. + +**Key Features:** + +* Full Site Editing with block templates and template parts +* 15+ custom Gutenberg blocks (hero, pricing cards, comparison tables, FAQ accordions, etc.) +* Dark / Light mode toggle with CSS custom properties +* Responsive design optimised for all screen sizes +* Canvas-based animated datacenter background +* AJAX-powered contact form +* Google Fonts (Inter) loaded for performance +* Block patterns for all 10 service and marketing pages + +== Installation == + +1. Upload the `theme` folder to `/wp-content/themes/oribi-tech` (rename the folder to `oribi-tech`). +2. Activate the theme through **Appearance β†’ Themes**. +3. Go to **Appearance β†’ Editor** to customise templates and template parts. +4. Create pages and apply the matching block pattern from the **Oribi Pages** category. +5. Set your home page under **Settings β†’ Reading β†’ A static page**. +6. Configure the primary and footer navigation menus under **Appearance β†’ Menus** or via the Site Editor. + +== Page Setup == + +Create a WordPress page for each service, then insert the corresponding block pattern: + +| Page Slug | Pattern Name | +| -------------------- | ------------------------- | +| / | Home Page | +| /about | About Page | +| /contact | Contact Page | +| /faq | FAQ Page | +| /managed-it | Managed IT Page | +| /365care | 365Care Page | +| /endpointcare | EndpointCare Page | +| /netcare | NetCare Page | +| /servercare | ServerCare Page | +| /services-comparison | Services Comparison Page | + +== Changelog == + += 1.0.0 = +* Initial release as a Full Site Editing block theme. +* Custom blocks: hero, page-hero, cta-banner, intro-section, contact-section, feature-section/card, pricing-section/card, platform-section/row, trust-section/item, faq-section/item, comparison-table, site-header, site-footer. +* FSE templates: front-page, page, single, index, archive, search, 404. +* Template parts: header, footer. +* 10 page patterns covering all marketing pages. +* Dark/light mode system. +* AJAX contact form. + +== Credits == + +* Inter font by Rasmus Andersson β€” https://rsms.me/inter/ (SIL Open Font License) +* Built with WordPress Full Site Editing and the Block API. diff --git a/theme/style.css b/theme/style.css new file mode 100644 index 0000000..e8edfa7 --- /dev/null +++ b/theme/style.css @@ -0,0 +1,14 @@ +/* +Theme Name: Oribi Tech +Theme URI: https://oribi-tech.com +Author: Oribi Technology Services +Author URI: https://oribi-tech.com +Description: Custom marketing theme for Oribi Technology Services β€” Managed IT, 365Care, EndpointCare, NetCare, ServerCare. +Version: 1.0.0 +Requires at least: 6.4 +Tested up to: 6.7 +Requires PHP: 7.4 +License: GNU General Public License v2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html +Text Domain: oribi-tech +*/ diff --git a/theme/templates/404.html b/theme/templates/404.html new file mode 100644 index 0000000..687d86c --- /dev/null +++ b/theme/templates/404.html @@ -0,0 +1,35 @@ + + + +
    + + + + +
    + +
    + + +

    The page you requested could not be found. It may have been moved or deleted.

    + + + + + + + + +
    + +
    + + +
    + + + diff --git a/theme/templates/archive.html b/theme/templates/archive.html new file mode 100644 index 0000000..6ccb7da --- /dev/null +++ b/theme/templates/archive.html @@ -0,0 +1,47 @@ + + + +
    + + +
    + +
    + + + + + + + +
    + + + +
    + + + + + + + + + + + +

    No posts found in this archive.

    + + + + + +
    + +
    + + +
    + + + diff --git a/theme/templates/front-page.html b/theme/templates/front-page.html new file mode 100644 index 0000000..06176f0 --- /dev/null +++ b/theme/templates/front-page.html @@ -0,0 +1,9 @@ + + + +
    + +
    + + + diff --git a/theme/templates/index.html b/theme/templates/index.html new file mode 100644 index 0000000..bd3685c --- /dev/null +++ b/theme/templates/index.html @@ -0,0 +1,45 @@ + + + +
    + + +
    + +
    + + + + + +
    + + + +
    + + + + + + + + + + + +

    No posts found.

    + + + + + +
    + +
    + + +
    + + + diff --git a/theme/templates/page.html b/theme/templates/page.html new file mode 100644 index 0000000..a6512ec --- /dev/null +++ b/theme/templates/page.html @@ -0,0 +1,9 @@ + + + +
    + +
    + + + diff --git a/theme/templates/search.html b/theme/templates/search.html new file mode 100644 index 0000000..694817a --- /dev/null +++ b/theme/templates/search.html @@ -0,0 +1,51 @@ + + + +
    + + +
    + +
    + + +

    Search Results

    + + + + + + + + +
    + + + +
    + + + + + + + + + + + +

    No results found. Please try a different search term.

    + + + + + +
    + +
    + + +
    + + + diff --git a/theme/templates/single.html b/theme/templates/single.html new file mode 100644 index 0000000..aed5bdf --- /dev/null +++ b/theme/templates/single.html @@ -0,0 +1,25 @@ + + + +
    + + +
    + +
    + + + + + + + +
    + +
    + + +
    + + + diff --git a/theme/theme.json b/theme/theme.json new file mode 100644 index 0000000..ef2d9d6 --- /dev/null +++ b/theme/theme.json @@ -0,0 +1,378 @@ +{ + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "layout": { + "contentSize": "1200px", + "wideSize": "1536px" + }, + "appearanceTools": false, + "border": { + "color": false, + "radius": false, + "style": false, + "width": false + }, + "shadow": { + "defaultPresets": false, + "presets": [] + }, + "spacing": { + "padding": false, + "margin": false, + "blockGap": false, + "customSpacingSize": false, + "units": ["px", "%", "em", "rem", "vw", "vh"] + }, + "color": { + "defaultPalette": false, + "defaultGradients": false, + "defaultDuotone": false, + "custom": true, + "customDuotone": false, + "customGradient": false, + "palette": [ + { "slug": "primary", "color": "#D83302", "name": "Primary" }, + { "slug": "primary-dk", "color": "#B52B02", "name": "Primary Dark" }, + { "slug": "primary-lt", "color": "#FEF0EB", "name": "Primary Light" }, + { "slug": "accent", "color": "#00757c", "name": "Accent" }, + { "slug": "accent-dk", "color": "#005a60", "name": "Accent Dark" }, + { "slug": "accent-lt", "color": "#E6F4F5", "name": "Accent Light" }, + { "slug": "dark", "color": "#0D1321", "name": "Dark" }, + { "slug": "dark-2", "color": "#1A2236", "name": "Dark 2" }, + { "slug": "text", "color": "#2D3748", "name": "Text" }, + { "slug": "text-muted", "color": "#718096", "name": "Text Muted" }, + { "slug": "border", "color": "#E2E8F0", "name": "Border" }, + { "slug": "bg", "color": "#FFFFFF", "name": "Background" }, + { "slug": "bg-alt", "color": "#FFF8F5", "name": "Background Alt" } + ] + }, + "typography": { + "defaultFontSizes": false, + "customFontSize": true, + "lineHeight": false, + "dropCap": false, + "textDecoration": false, + "textTransform": false, + "fontStyle": false, + "fontWeight": false, + "letterSpacing": false, + "fontFamilies": [ + { + "fontFamily": "'Inter', system-ui, -apple-system, sans-serif", + "name": "Body", + "slug": "sans" + }, + { + "fontFamily": "'Inter', system-ui, -apple-system, sans-serif", + "name": "Heading", + "slug": "heading" + } + ], + "fontSizes": [ + { "slug": "xs", "size": "0.75rem", "name": "XS β€” 12px" }, + { "slug": "sm", "size": "0.875rem", "name": "SM β€” 14px" }, + { "slug": "base", "size": "1rem", "name": "Base β€” 16px" }, + { "slug": "md", "size": "1.125rem", "name": "MD β€” 18px" }, + { "slug": "lg", "size": "1.25rem", "name": "LG β€” 20px" }, + { "slug": "xl", "size": "1.5rem", "name": "XL β€” 24px" }, + { "slug": "2xl", "size": "1.875rem", "name": "2XL β€” 30px" }, + { "slug": "3xl", "size": "2.25rem", "name": "3XL β€” 36px" }, + { "slug": "4xl", "size": "3rem", "name": "4XL β€” 48px" }, + { "slug": "5xl", "size": "3.75rem", "name": "5XL β€” 60px" }, + { "slug": "6xl", "size": "4.5rem", "name": "6XL β€” 72px" } + ] + }, + "custom": { + "dark": { + "primary": "#FF6B3D", + "primary-dk": "#D83302", + "primary-lt": "rgba(216,51,2,0.15)", + "accent": "#00757c", + "accent-dk": "#005a60", + "accent-lt": "rgba(0,117,124,0.15)", + "dark": "#E2E8F0", + "dark-2": "#CBD5E0", + "text": "#CBD5E0", + "text-muted": "#A0AEC0", + "border": "#2D3748", + "bg": "#0F1724", + "bg-alt": "#151F30", + "bg-dark": "#0A0F1A", + "heading": "#F7FAFC", + "card-bg": "#151F30" + }, + "radius": { + "sm": "6px", + "md": "12px", + "lg": "20px", + "xl": "32px" + }, + "container": { + "max": "1200px", + "pad": "clamp(1rem, 5vw, 2rem)" + } + }, + "useRootPaddingAwareAlignments": false + }, + "styles": { + "color": { + "text": "var:preset|color|text", + "background": "var:preset|color|bg" + }, + "typography": { + "fontFamily": "var:preset|font-family|sans", + "fontSize": "var:preset|font-size|base" + }, + "spacing": { + "blockGap": "0", + "padding": "0", + "margin": "0" + }, + "elements": { + "link": { + "color": { + "text": "var:preset|color|primary" + }, + "typography": { + "textDecoration": "none" + }, + ":hover": { + "color": { + "text": "var:preset|color|primary-dk" + }, + "typography": { + "textDecoration": "underline" + } + }, + ":focus": { + "color": { + "text": "var:preset|color|primary-dk" + } + } + }, + "button": { + "color": { + "text": "var:preset|color|bg", + "background": "var:preset|color|primary" + }, + "typography": { + "fontFamily": "var:preset|font-family|sans", + "fontSize": "var:preset|font-size|base" + }, + ":hover": { + "color": { + "background": "var:preset|color|primary-dk" + } + }, + ":focus": { + "color": { + "background": "var:preset|color|primary-dk" + } + } + }, + "h1": { + "typography": { + "fontFamily": "var:preset|font-family|heading" + } + }, + "h2": { + "typography": { + "fontFamily": "var:preset|font-family|heading" + } + }, + "h3": { + "typography": { + "fontFamily": "var:preset|font-family|heading" + } + }, + "h4": { + "typography": { + "fontFamily": "var:preset|font-family|heading" + } + }, + "h5": { + "typography": { + "fontFamily": "var:preset|font-family|heading" + } + }, + "h6": { + "typography": { + "fontFamily": "var:preset|font-family|heading" + } + } + }, + "blocks": { + "core/group": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "core/columns": { + "spacing": { + "padding": "0", + "margin": "0", + "blockGap": "0" + } + }, + "core/column": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "core/heading": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "core/paragraph": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "core/list": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "core/image": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "core/separator": { + "color": { + "text": "var:preset|color|border" + } + }, + "core/button": { + "color": { + "text": "var:preset|color|bg", + "background": "var:preset|color|primary" + }, + "typography": { + "fontFamily": "var:preset|font-family|sans", + "fontSize": "var:preset|font-size|base" + } + }, + "oribi/hero": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/page-hero": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/cta-banner": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/intro-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/contact-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/feature-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/feature-card": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/pricing-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/pricing-card": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/platform-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/platform-row": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/faq-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/faq-item": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/comparison-table": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/trust-section": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/trust-item": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/hero-animated": { + "spacing": { + "padding": "0", + "margin": "0" + } + }, + "oribi/page-hero-animated": { + "spacing": { + "padding": "0", + "margin": "0" + } + } + } + }, + "templateParts": [ + { "name": "header", "title": "Header", "area": "header" }, + { "name": "footer", "title": "Footer", "area": "footer" } + ] +}