/** * URBAN RENDER LIBRARY v1.2 * * The frontend twin of the Urban Studio Framework. The engine made ADMIN * screens universal — one engine, one config per site. Templates stayed * per-site forks, and forking templates is where this project's worst bugs * were born: Raymond's taxonomy inside Hiranandani's hub, a CSS class whose * rules existed in only one of the two stacks. This library exists to make * forking frontend code extinct the same way. * * ── THE THREE LAWS ───────────────────────────────────────────────────── * * 1. COMPONENTS OWN THEIR CSS. * A component emits its own styles, once per request. There is no "which * style block does this rule belong in?" question to get wrong, and a * component copied to another site cannot arrive unstyled. * * 2. COMPONENTS SPEAK ONLY IN TOKENS — with one stated exception. * * THIRD-PARTY BRAND COLOURS STAY LITERAL. YouTube red, the Instagram * gradient and Google Maps green are not part of any site's palette and no * site may change them: a green YouTube button is wrong on every site that * could ever use this library. Tokenising them would invite exactly that. * They are hard-coded deliberately, and they are the ONLY hex literals * permitted in a component. * * (The first draft tokenised the Maps green while leaving YouTube red * literal — an inconsistency caught by the audit, not by review.) * * No hex literal may appear in a component. Every colour is a --up-* * custom property supplied by the site's theme. This makes the Contrast * Sweep structural: a palette is validated once per site rather than per * selector, so white-on-cream cannot be introduced by a new component. * * 3. SAME BY DEFAULT, DIVERGENT ONLY WHEN DECLARED. * A site renders the default component. It may differ only by naming a * variant in its theme config. A named variant must consume the IDENTICAL * data contract as the default and honour the same empty-hides behaviour — * it may differ in markup and styling only. The moment a variant needs its * own field, it is not a variant, it is a second component and gets its own * name. Presentation may diverge; the contract may not. * * ── WHAT IS NOT HERE ─────────────────────────────────────────────────── * Header, footer, sticky bar and floating elements stay with each site. * They are the most site-personal things on the page and absorbing them * would make the library a theme rather than a component set. * * ── SCOPE OF v1.0 ────────────────────────────────────────────────────── * The seven components the facet page needs. The library is born with new * code, not by extracting the live project and variant templates: the facet * page is new, so nothing can break while the library proves itself. The * existing templates migrate onto these components afterwards, one at a * time, each with its own before/after check. */ if ( ! defined( 'ABSPATH' ) ) { exit; } /* ═══════════════════════════════════════════════════════════════════════ * THEME LAYER — a site declares its palette; components consume names only * ═══════════════════════════════════════════════════════════════════════ */ if ( ! isset( $GLOBALS['up_theme'] ) ) { $GLOBALS['up_theme'] = array(); } if ( ! function_exists( 'up_theme_register' ) ) { function up_theme_register( array $tokens, array $opts = array() ): void { $GLOBALS['up_theme'] = array( 'tokens' => $tokens, 'opts' => $opts ); } } /** The site's chosen variant for a component, or '' for the default. */ if ( ! function_exists( 'up_variant_of' ) ) { function up_variant_of( string $component ): string { return (string) ( $GLOBALS['up_theme']['opts']['components'][ $component ] ?? '' ); } } /** * Emit the palette once. Every component depends on this having run, so each * one calls it; the static guard makes the repetition free. */ if ( ! function_exists( 'up_theme_css' ) ) { function up_theme_css(): void { static $done = false; if ( $done ) { return; } $done = true; $defaults = array( 'primary' => '#7B1E2E', 'primary-deep' => '#5A1220', 'accent' => '#C9A45C', 'accent-soft' => '#E7D3A8', 'ink' => '#3E4A5E', 'mute' => '#8A8378', 'line' => '#EBD9C0', 'dark' => '#0B0B0D', 'surface' => '#FFFFFF', 'surface-warm' => '#FBF3EC', 'surface-alt' => '#F6E7D4', 'on-primary' => '#FFFFFF', // text ON a primary-coloured surface /* Added for the listing components. Every one has a real use; a token * that exists for tidiness rather than need is a token that drifts. */ 'accent' => '#C9A45C', // aliased below for sites that call it that 'accent-ink' => '#7A4E00', // accent, dark enough to read as text 'on-accent' => '#FFFFFF', 'soft-accent' => '#FDF1E1', 'soft-green' => '#E1F7E8', 'verified' => '#1D9E75', 'verified-ink' => '#0E6B50', 'whatsapp' => '#25D366', 'line-soft' => '#F0EDDF', 'radius-sm' => '8px', 'radius-md' => '12px', 'radius-lg' => '16px', 'radius-pill' => '999px', 'shadow-sm' => '0 4px 12px rgba(0,0,0,.06)', 'shadow-md' => '0 12px 30px rgba(0,0,0,.08)', 'font-display' => "'Playfair Display',serif", 'font-body' => "'Inter',sans-serif", 'container' => '1160px', ); $tokens = array_merge( $defaults, (array) ( $GLOBALS['up_theme']['tokens'] ?? array() ) ); echo ''; } } /** * Emit a component's CSS exactly once per request. * * This is Law 1 made mechanical: a component cannot be rendered without its * styles, and rendering it fifty times cannot emit them fifty times. */ if ( ! function_exists( 'up_css' ) ) { function up_css( string $id, string $css ): void { static $seen = array(); if ( isset( $seen[ $id ] ) ) { return; } $seen[ $id ] = true; up_theme_css(); echo ''; } } /** Shared container rule, needed by nearly every component. */ if ( ! function_exists( 'up_wrap_css' ) ) { function up_wrap_css(): void { up_css( 'wrap', '.up-wrap{max-width:var(--up-container);margin:0 auto;padding:0 24px}' . '.up-eyebrow{font-size:.7rem;letter-spacing:.32em;text-transform:uppercase;color:var(--up-accent);font-weight:600}' . '.up-sec-head{text-align:center;margin-bottom:40px}' . '.up-sec-head h2{font-family:var(--up-font-display);font-weight:600;color:var(--up-primary);font-size:clamp(1.6rem,2.6vw,2.2rem);margin:12px 0 0}' . '.up-sec-head h2 em{font-style:italic;color:var(--up-accent)}' ); } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 1 — BREADCRUMB * data: [ ['name'=>string, 'url'=>string|''], … ] last item renders plain * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_breadcrumb' ) ) { function up_render_breadcrumb( array $crumbs ): void { $crumbs = array_values( array_filter( $crumbs, function ( $c ) { return '' !== trim( (string) ( $c['name'] ?? '' ) ); } ) ); if ( ! $crumbs ) { return; } // empty hides up_wrap_css(); up_css( 'crumb', '.up-crumb{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--up-mute);margin-bottom:22px}' . '.up-crumb a{color:var(--up-mute);text-decoration:none}' . '.up-crumb a:hover{color:var(--up-primary)}' . '.up-crumb span{color:var(--up-primary)}' ); echo ''; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 2 — PAGE HERO (authored) * data: eyebrow, heading (may contain ), intro (plain text, \n\n = para) * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_page_hero' ) ) { function up_render_page_hero( array $d ): void { $heading = trim( (string) ( $d['heading'] ?? '' ) ); $intro = trim( (string) ( $d['intro'] ?? '' ) ); $eyebrow = trim( (string) ( $d['eyebrow'] ?? '' ) ); if ( '' === $heading && '' === $intro ) { return; } // empty hides up_wrap_css(); up_css( 'hero', '.up-hero{background:var(--up-surface-warm);padding:56px 0 48px;border-bottom:1px solid var(--up-accent-soft)}' . '.up-hero h1{font-family:var(--up-font-display);font-weight:600;color:var(--up-primary);' . 'font-size:clamp(2rem,4vw,3rem);line-height:1.15;margin:14px 0 18px}' . '.up-hero h1 em{font-style:italic;color:var(--up-accent)}' . '.up-hero-intro{max-width:720px;color:var(--up-ink);font-size:1rem;line-height:1.7}' . '.up-hero-intro p{margin:0 0 12px}' . '.up-hero-intro p:last-child{margin-bottom:0}' ); echo '
'; if ( ! empty( $d['crumbs'] ) ) { up_render_breadcrumb( (array) $d['crumbs'] ); } if ( '' !== $eyebrow ) { echo '' . esc_html( $eyebrow ) . ''; } if ( '' !== $heading ) { echo '

' . wp_kses( $heading, array( 'em' => array() ) ) . '

'; } if ( '' !== $intro ) { echo '
'; foreach ( preg_split( '/\n\s*\n/', $intro ) as $para ) { $para = trim( $para ); if ( '' !== $para ) { echo '

' . esc_html( $para ) . '

'; } } echo '
'; } echo '
'; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 3 — FACTS BAR (computed) * data: [ ['value'=>string,'label'=>string], … ] * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_facts_bar' ) ) { function up_render_facts_bar( array $cells ): void { $cells = array_values( array_filter( $cells, function ( $c ) { return '' !== trim( (string) ( $c['value'] ?? '' ) ); } ) ); if ( ! $cells ) { return; } // empty hides up_wrap_css(); up_css( 'facts', '.up-facts{background:var(--up-dark);padding:26px 0}' . '.up-facts-grid{display:grid;gap:12px;text-align:center}' . '.up-facts b{display:block;font-family:var(--up-font-display);font-weight:600;' . 'color:var(--up-accent-soft);font-size:1.5rem;line-height:1.2}' . '.up-facts span{font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;color:var(--up-mute)}' . '@media(max-width:900px){.up-facts-grid{grid-template-columns:repeat(2,1fr)!important;gap:20px}}' ); echo '
'; foreach ( $cells as $c ) { echo '
' . esc_html( (string) $c['value'] ) . '' . esc_html( (string) ( $c['label'] ?? '' ) ) . '
'; } echo '
'; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 4 — PROJECT CARDS * data: [ ['title','url','image','kicker','meta','price','cta'], … ] * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_project_cards' ) ) { function up_render_project_cards( array $cards, array $head = array() ): void { $cards = array_values( array_filter( $cards, function ( $c ) { return '' !== trim( (string) ( $c['title'] ?? '' ) ); } ) ); if ( ! $cards ) { return; } // empty hides up_wrap_css(); up_css( 'cards', '.up-sec{padding:72px 0}' . '.up-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}' . '.up-card{border:1px solid var(--up-line);background:var(--up-surface);display:flex;flex-direction:column;' . 'transition:transform .25s ease,box-shadow .25s ease}' . '.up-card:hover{transform:translateY(-5px);box-shadow:0 16px 40px rgba(0,0,0,.12)}' . '.up-card-img{aspect-ratio:4/3;background:linear-gradient(135deg,var(--up-primary),var(--up-primary-deep));' . 'background-size:cover;background-position:center;position:relative}' . '.up-card-img i{position:absolute;right:10px;bottom:8px;font-style:normal;font-size:.58rem;' . 'letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.55)}' . '.up-card-body{padding:22px;background:var(--up-surface-alt);flex:1;display:flex;flex-direction:column}' . '.up-card-kicker{font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;color:var(--up-accent);font-weight:600}' . '.up-card h3{font-family:var(--up-font-display);font-weight:600;color:var(--up-primary);font-size:1.25rem;margin:8px 0 12px}' . '.up-card-meta{font-size:.86rem;color:var(--up-ink);border-top:1px solid var(--up-line);padding-top:12px;margin-top:auto}' . '.up-card-price{font-family:var(--up-font-display);font-style:italic;color:var(--up-primary);font-size:1.05rem;margin-top:6px}' . '.up-card-go{margin-top:16px;font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;color:var(--up-primary);' . 'font-weight:600;text-decoration:none;border-bottom:1px solid var(--up-accent);padding-bottom:3px;align-self:flex-start}' . '@media(max-width:900px){.up-cards{grid-template-columns:1fr}.up-sec{padding:52px 0}}' ); echo '
'; if ( ! empty( $head['heading'] ) || ! empty( $head['eyebrow'] ) ) { echo '
'; if ( ! empty( $head['eyebrow'] ) ) { echo '' . esc_html( (string) $head['eyebrow'] ) . ''; } if ( ! empty( $head['heading'] ) ) { echo '

' . wp_kses( (string) $head['heading'], array( 'em' => array() ) ) . '

'; } echo '
'; } echo '
'; foreach ( $cards as $c ) { $img = (string) ( $c['image'] ?? '' ); echo '
'; echo '
'; if ( ! empty( $c['image_note'] ) ) { echo '' . esc_html( (string) $c['image_note'] ) . ''; } echo '
'; if ( ! empty( $c['kicker'] ) ) { echo '' . esc_html( (string) $c['kicker'] ) . ''; } echo '

' . esc_html( (string) $c['title'] ) . '

'; echo '
' . esc_html( (string) ( $c['meta'] ?? '' ) ); if ( ! empty( $c['price'] ) ) { echo '
' . esc_html( (string) $c['price'] ) . '
'; } echo '
'; if ( ! empty( $c['url'] ) ) { echo '' . esc_html( (string) ( $c['cta'] ?? 'Discover the project' ) ) . ' →'; } echo '
'; } echo '
'; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 5 — CROSS LINKS * data: [ ['title'=>string, 'links'=>[ ['name','url'], … ] ], … ] * What turns isolated facet pages into a network rather than orphans. * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_cross_links' ) ) { function up_render_cross_links( array $cols ): void { $cols = array_values( array_filter( $cols, function ( $c ) { return ! empty( $c['links'] ); } ) ); if ( ! $cols ) { return; } // empty hides up_wrap_css(); up_css( 'xlinks', '.up-xlinks{background:var(--up-surface-warm);padding:56px 0}' . '.up-xgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:34px}' . '.up-xcol h4{font-family:var(--up-font-display);color:var(--up-primary);font-size:1rem;margin:0 0 12px}' . '.up-xcol a{display:block;color:var(--up-ink);text-decoration:none;font-size:.9rem;padding:6px 0;' . 'border-bottom:1px solid var(--up-line)}' . '.up-xcol a:hover{color:var(--up-primary)}' . '@media(max-width:900px){.up-xgrid{grid-template-columns:1fr;gap:26px}}' ); echo ''; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 6 — FAQ * data: [ ['q'=>string,'a'=>string], … ] * Renders the accordion AND returns the FAQPage schema nodes, so the two can * never disagree — a question shown but not marked up, or the reverse, is a * bug that only appears months later in Search Console. * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_faq' ) ) { function up_render_faq( array $faqs, array $head = array() ): array { $clean = array(); foreach ( $faqs as $f ) { $q = trim( (string) ( $f['q'] ?? '' ) ); $a = trim( (string) ( $f['a'] ?? '' ) ); if ( '' !== $q && '' !== $a ) { $clean[] = array( 'q' => $q, 'a' => $a ); } } if ( ! $clean ) { return array(); } // empty hides, and emits no schema up_wrap_css(); up_css( 'faq', '.up-faq{padding:72px 0}' . '.up-faq details{border-bottom:1px solid var(--up-line);padding:16px 0}' . '.up-faq summary{cursor:pointer;font-family:var(--up-font-display);color:var(--up-primary);font-size:1.05rem;' . 'list-style:none;display:flex;justify-content:space-between;align-items:center;gap:16px;' . '-webkit-user-select:none;user-select:none}' . '.up-faq summary::-webkit-details-marker{display:none}' . '.up-faq summary::after{content:"+";color:var(--up-accent);font-size:1.3rem;font-family:var(--up-font-body)}' . '.up-faq details[open] summary::after{content:"–"}' . '.up-faq p{margin:12px 0 0;color:var(--up-ink);font-size:.94rem;max-width:820px;line-height:1.7}' . '@media(max-width:900px){.up-faq{padding:52px 0}}' ); echo '
'; if ( ! empty( $head['heading'] ) || ! empty( $head['eyebrow'] ) ) { echo '
'; if ( ! empty( $head['eyebrow'] ) ) { echo '' . esc_html( (string) $head['eyebrow'] ) . ''; } if ( ! empty( $head['heading'] ) ) { echo '

' . wp_kses( (string) $head['heading'], array( 'em' => array() ) ) . '

'; } echo '
'; } $schema = array(); foreach ( $clean as $i => $f ) { echo ''; echo '' . esc_html( $f['q'] ) . ''; echo '

' . esc_html( $f['a'] ) . '

'; echo ''; $schema[] = array( '@type' => 'Question', 'name' => $f['q'], 'acceptedAnswer' => array( '@type' => 'Answer', 'text' => $f['a'] ), ); } echo '
'; return $schema; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT 7 — ENQUIRY FORM * * The library owns the MECHANICS: markup, styling, the hidden fields, the * return-URL field, the honeypot. The site supplies where a lead LANDS via * 'action' and its own hidden fields — because the lead post type differs per * site and the library must never learn a post type name. * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_enquiry' ) ) { function up_render_enquiry( array $d ): void { $action = (string) ( $d['action'] ?? '' ); if ( '' === $action ) { return; } // no handler declared → no form up_wrap_css(); up_css( 'enq', '.up-enq{background:var(--up-dark);padding:64px 0;color:var(--up-accent-soft)}' . '.up-enq-grid{display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:center}' . '.up-enq h2{font-family:var(--up-font-display);color:var(--up-accent-soft);font-size:1.8rem;margin:12px 0 14px}' . '.up-enq h2 em{font-style:italic;color:var(--up-accent)}' . '.up-enq p{color:var(--up-mute);font-size:.92rem;line-height:1.7}' . '.up-enq input,.up-enq select{width:100%;padding:13px 2px;border:0;border-bottom:1px solid var(--up-mute);' . 'background:transparent;color:var(--up-accent-soft);margin-bottom:16px;font-family:var(--up-font-body);font-size:.94rem}' . '.up-enq input::placeholder{color:var(--up-mute)}' . '.up-enq button{background:var(--up-primary);color:var(--up-on-primary);border:1px solid var(--up-accent);padding:14px 30px;' . 'font-size:.74rem;letter-spacing:.2em;text-transform:uppercase;font-weight:600;cursor:pointer;' . '-webkit-user-select:none;user-select:none;transition:background .2s ease}' . '.up-enq button:hover{background:var(--up-primary-deep)}' . '.up-enq-hp{position:absolute;left:-10000px;opacity:0;pointer-events:none}' . '@media(max-width:900px){.up-enq-grid{grid-template-columns:1fr;gap:28px}}' ); echo '
'; echo '
'; if ( ! empty( $d['eyebrow'] ) ) { echo '' . esc_html( (string) $d['eyebrow'] ) . ''; } if ( ! empty( $d['heading'] ) ) { echo '

' . wp_kses( (string) $d['heading'], array( 'em' => array() ) ) . '

'; } if ( ! empty( $d['blurb'] ) ) { echo '

' . esc_html( (string) $d['blurb'] ) . '

'; } echo '
'; echo '
'; echo ''; foreach ( (array) ( $d['hidden'] ?? array() ) as $k => $v ) { echo ''; } if ( ! empty( $d['nonce_action'] ) && ! empty( $d['nonce_name'] ) ) { wp_nonce_field( (string) $d['nonce_action'], (string) $d['nonce_name'] ); } /* Honeypot: off-screen, never visible, never focusable. * * The NAME is supplied by the site, because the site's handler is what * checks it. A library that hardcoded "website" while a handler looked for * "uhp2_web" would render a honeypot that can never fire — spam protection * that exists in the markup and nowhere else. Field names are a contract * between form and handler, so the contract holder names them. */ $hp = (string) ( $d['honeypot'] ?? 'website' ); echo ''; $pre = (array) ( $d['prefill'] ?? array() ); echo ''; echo ''; if ( ! empty( $d['intents'] ) ) { echo ''; } echo ''; echo '
'; echo '
'; } } /* ═══════════════════════════════════════════════════════════════════════ * COMPONENT REGISTRY — so the estate's divergence is inspectable rather * than folkloric. A component with no registered variants has exactly one * implementation, by definition. * ═══════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_components' ) ) { function up_components(): array { return array( 'breadcrumb' => array( 'fn' => 'up_render_breadcrumb', 'variants' => array() ), 'page_hero' => array( 'fn' => 'up_render_page_hero', 'variants' => array() ), 'facts_bar' => array( 'fn' => 'up_render_facts_bar', 'variants' => array() ), 'project_cards' => array( 'fn' => 'up_render_project_cards', 'variants' => array() ), 'cross_links' => array( 'fn' => 'up_render_cross_links', 'variants' => array() ), 'faq' => array( 'fn' => 'up_render_faq', 'variants' => array() ), 'enquiry' => array( 'fn' => 'up_render_enquiry', 'variants' => array() ), /* listing components — added Sep 3 2026 */ 'gallery' => array( 'fn' => 'up_render_gallery', 'variants' => array() ), 'record' => array( 'fn' => 'up_render_record', 'variants' => array() ), 'section_nav' => array( 'fn' => 'up_render_section_nav', 'variants' => array() ), 'spec_tables' => array( 'fn' => 'up_render_spec_tables', 'variants' => array() ), 'chips' => array( 'fn' => 'up_render_chips', 'variants' => array() ), 'address' => array( 'fn' => 'up_render_address', 'variants' => array() ), 'media_links' => array( 'fn' => 'up_render_media_links', 'variants' => array() ), 'gate' => array( 'fn' => 'up_render_gate', 'variants' => array() ), 'listing_cards' => array( 'fn' => 'up_render_listing_cards', 'variants' => array() ), 'prose' => array( 'fn' => 'up_render_prose', 'variants' => array() ), ); } } /* ═══════════════════════════════════════════════════════════════════════ * LISTING COMPONENTS (added Sep 3 2026, for the main-site property page) * * Built from the approved mockup, NOT extracted from Hiranandani's 5a. * 5a's markup is burgundy and shaped for project and variant pages; the * listing page is a different design — cream page, sticky record card, * different spec grouping. Lifting 5a would have produced Hiranandani's page * in orange rather than the page that was signed off. * * Hiranandani migrates ONTO these later, in the other direction: replacing * its inline markup with calls, once these are proven here. * * Every component obeys the three laws: owns its CSS, speaks only in tokens, * and hides itself when it has nothing to show. * ═══════════════════════════════════════════════════════════════════════ */ /* ═══ 8. GALLERY ══════════════════════════════════════════════════════ * data: array of attachment IDs. * Desktop a 2:1 split — one large image, two stacked. Mobile a full-bleed * swipe strip: photographs are the product on a resale and a phone is where * people actually look at them. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_gallery' ) ) { function up_render_gallery( array $ids, array $opts = array() ) { $ids = array_values( array_filter( array_map( 'intval', $ids ) ) ); if ( ! $ids ) { return; } // empty hides up_wrap_css(); up_css( 'gallery', '.up-gal{display:grid;grid-template-columns:2fr 1fr;grid-template-rows:158px 158px;gap:10px}' . '.up-gal figure{margin:0;border-radius:var(--up-radius-md);overflow:hidden;position:relative;' . 'box-shadow:var(--up-shadow-sm);background:var(--up-surface-warm)}' . '.up-gal figure:first-child{grid-row:1/3;border-radius:var(--up-radius-lg)}' . '.up-gal img{width:100%;height:100%;object-fit:cover;display:block}' . '.up-gal-more{position:absolute;right:12px;bottom:12px;background:rgba(0,0,0,.78);color:#fff;' . 'font-size:12.5px;padding:7px 13px;border-radius:var(--up-radius-pill)}' . '.up-gal-tag{position:absolute;left:12px;top:12px;font-size:12px;font-weight:600;padding:6px 12px;' . 'border-radius:var(--up-radius-pill);background:var(--up-soft-green);color:var(--up-verified)}' /* Mobile: one full-bleed swipe strip. A 2:1 grid on a 390px screen * makes the two small images unreadable. */ . '@media(max-width:900px){' . '.up-gal{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:0;' . 'grid-template-columns:none;grid-template-rows:none;scrollbar-width:none;' . 'margin:0 -16px;border-radius:0}' . '.up-gal::-webkit-scrollbar{display:none}' . '.up-gal figure{flex:none;width:100vw;height:270px;border-radius:0!important;' . 'scroll-snap-align:start;box-shadow:none}' . '}' ); $tag = (string) ( $opts['tag'] ?? '' ); $total = count( $ids ); $show = array_slice( $ids, 0, 3 ); echo '
'; foreach ( $show as $i => $id ) { $size = ( 0 === $i ) ? 'large' : 'medium_large'; $url = wp_get_attachment_image_url( $id, $size ); if ( ! $url ) { continue; } echo '
'; echo '' . esc_attr( get_post_meta( $id, '_wp_attachment_image_alt', true ) ) . ''; if ( 0 === $i && '' !== $tag ) { echo '' . esc_html( $tag ) . ''; } if ( 0 === $i && $total > 1 ) { echo '1 / ' . (int) $total . ' photos'; } echo '
'; } echo '
'; } } /* ═══ 9. RECORD CARD ══════════════════════════════════════════════════ * The signature element. Price, the facts that matter, verification, and * one clear action — sticky beside the gallery through the whole page, so a * buyer comparing tabs always has the numbers in view. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_record' ) ) { function up_render_record( array $d ) { if ( '' === trim( (string) ( $d['title'] ?? '' ) ) ) { return; } up_wrap_css(); up_css( 'record', '.up-rec{background:var(--up-surface);border-radius:var(--up-radius-lg);' . 'box-shadow:var(--up-shadow-md);position:sticky;top:96px;overflow:hidden}' . '.up-rec-h{padding:22px 24px 18px}' . '.up-rec h1{font-family:var(--up-font-display);font-size:20px;line-height:1.35;margin:0;' . 'font-weight:600;color:var(--up-ink)}' . '.up-rec-loc{font-size:14px;color:var(--up-mute);margin-top:6px}' . '.up-rec-price{padding:0 24px 6px}' . '.up-rec-amt{font-family:var(--up-font-display);font-size:34px;font-weight:700;' . 'letter-spacing:-.02em;line-height:1.1;color:var(--up-ink)}' . '.up-rec-per{font-size:13.5px;color:var(--up-mute);margin-top:4px}' . '.up-rec-neg{display:inline-block;font-size:12.5px;color:var(--up-accent-ink);' . 'background:var(--up-soft-accent);padding:4px 12px;border-radius:var(--up-radius-pill);margin-top:12px}' . '.up-rec-facts{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:20px 24px 6px}' . '.up-rec-facts div{background:var(--up-surface-warm);border-radius:var(--up-radius-sm);padding:12px 14px}' . '.up-rec-facts b{display:block;font-size:16px;font-weight:600}' . '.up-rec-facts span{font-size:12px;color:var(--up-mute)}' . '.up-rec-verify{margin:16px 24px 0;background:var(--up-soft-green);' . 'border-radius:var(--up-radius-sm);padding:12px 14px;font-size:12.5px;' . 'color:var(--up-verified-ink);display:flex;gap:9px;align-items:flex-start;line-height:1.55}' . '.up-rec-cta{padding:16px 24px 10px;display:grid;gap:10px}' . '.up-btn{border:0;cursor:pointer;border-radius:var(--up-radius-pill);' . 'font:600 15px var(--up-font-body);padding:14px;text-align:center;text-decoration:none;' . 'display:flex;align-items:center;justify-content:center;gap:9px;transition:background .2s ease}' . '.up-btn-primary{background:var(--up-accent);color:var(--up-on-accent)}' . '.up-btn-wa{background:var(--up-whatsapp);color:#fff}' . '.up-btn-ghost{background:var(--up-soft-accent);border:1.5px solid var(--up-accent);' . 'color:var(--up-accent-ink);font-weight:600}' . '.up-rec-by{margin:6px 24px 22px;border-top:1px solid var(--up-line);padding-top:16px;' . 'display:flex;gap:12px;align-items:flex-start}' . '.up-rec-av{width:40px;height:40px;border-radius:var(--up-radius-pill);background:var(--up-accent);' . 'color:var(--up-on-accent);display:grid;place-items:center;font-weight:700;flex:none}' . '.up-rec-by b{font-size:14px;display:block}' . '.up-rec-by span{font-size:12.5px;color:var(--up-mute);line-height:1.5}' . '.up-rec-ver{color:var(--up-verified);font-weight:600}' . '@media(max-width:1000px){.up-rec{position:static}}' ); echo ''; } } /* ═══ 10. SECTION NAV ═════════════════════════════════════════════════ * Every button always visible; a section with content is a link, one without * is dimmed and inert. It lights up on its own as a listing is filled in — * which tells the person entering it what is still missing, for free. * Stores nothing: derived entirely from which sections produced output. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_section_nav' ) ) { function up_render_section_nav( array $items ) { if ( ! $items ) { return; } up_wrap_css(); up_css( 'secnav', '.up-snav{background:var(--up-surface);border-top:1px solid var(--up-line);' . 'border-bottom:1px solid var(--up-line);position:sticky;top:72px;z-index:30}' . '.up-snav-in{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;' . 'max-width:var(--up-container);margin:0 auto;padding:0 24px}' . '.up-snav-in::-webkit-scrollbar{display:none}' . '.up-snav a{padding:15px 18px;font-size:14px;color:var(--up-mute);text-decoration:none;' . 'white-space:nowrap;border-bottom:3px solid transparent}' . '.up-snav a:hover{color:var(--up-ink)}' . '.up-snav a.on{color:var(--up-accent-ink);border-bottom-color:var(--up-accent);font-weight:600}' . '.up-snav span{padding:15px 18px;font-size:14px;color:var(--up-line);white-space:nowrap;' . 'cursor:default;-webkit-user-select:none;user-select:none}' ); echo ''; } } /* ═══ 11. SPEC TABLES ═════════════════════════════════════════════════ * data: array of groups, each [ 'title' => string, 'rows' => [label=>value] ] * * Hairline rows, not cards. Chopping data into identical rounded boxes is the * generic default and it makes comparison HARDER — which is the one job a * spec table has. Empty hides at BOTH levels: a row with no value does not * render, and a group left with no rows does not render either. That is why a * commercial group never appears on a flat without any conditional here. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_spec_tables' ) ) { function up_render_spec_tables( array $groups ) { $clean = array(); foreach ( $groups as $g ) { $rows = array(); foreach ( (array) ( $g['rows'] ?? array() ) as $label => $value ) { if ( '' === trim( (string) $value ) ) { continue; } $rows[ $label ] = $value; } if ( $rows ) { $clean[] = array( 'title' => $g['title'] ?? '', 'rows' => $rows ); } } if ( ! $clean ) { return; } up_wrap_css(); up_css( 'specs', '.up-specs{display:grid;grid-template-columns:1fr 1fr;gap:0 56px}' . '.up-spec-g{margin-bottom:26px;break-inside:avoid}' . '.up-spec-g h3{font-size:13px;font-weight:600;color:var(--up-mute);margin:0 0 8px;' . 'font-family:var(--up-font-body)}' . '.up-spec-r{display:flex;justify-content:space-between;gap:20px;padding:10px 0;' . 'border-bottom:1px solid var(--up-line-soft)}' . '.up-spec-r dt{color:var(--up-mute)}' . '.up-spec-r dd{margin:0;font-weight:600;text-align:right}' . '.up-specs dl{margin:0}' . '@media(max-width:900px){.up-specs{grid-template-columns:1fr;gap:0}}' ); echo '
'; $half = (int) ceil( count( $clean ) / 2 ); foreach ( array( array_slice( $clean, 0, $half ), array_slice( $clean, $half ) ) as $col ) { echo '
'; foreach ( $col as $g ) { echo '
'; if ( '' !== $g['title'] ) { echo '

' . esc_html( $g['title'] ) . '

'; } echo '
'; foreach ( $g['rows'] as $label => $value ) { echo '
' . esc_html( $label ) . '
' . '
' . esc_html( $value ) . '
'; } echo '
'; } echo '
'; } echo '
'; } } /* ═══ 12. AMENITY CHIPS ═══════════════════════════════════════════════ * data: array of slug => label, plus an optional emoji map. * The map is GENERATED from the preset's real keys by the caller, never typed * from its labels — 9 of 28 emoji rendered as a diamond on Hiranandani for * exactly that reason. (Key Provenance Rule.) * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_chips' ) ) { function up_render_chips( array $items, array $emoji = array() ) { $items = array_filter( $items, function ( $v ) { return '' !== trim( (string) $v ); } ); if ( ! $items ) { return; } up_wrap_css(); up_css( 'chips', '.up-chips{display:flex;flex-wrap:wrap;gap:9px}' . '.up-chip{background:var(--up-surface-warm);border-radius:var(--up-radius-pill);' . 'padding:9px 16px;font-size:13.5px;color:var(--up-ink);display:flex;gap:8px;align-items:center}' . '.up-chip i{font-style:normal;font-size:15px;line-height:1}' . '.up-chip b{font-weight:600}' ); echo '
'; foreach ( $items as $slug => $label ) { echo ''; if ( isset( $emoji[ $slug ] ) && '' !== $emoji[ $slug ] ) { echo '' . esc_html( $emoji[ $slug ] ) . ''; } echo esc_html( $label ) . ''; } echo '
'; } } /* ═══ 13. ADDRESS BLOCK ═══════════════════════════════════════════════ * The one genuine extraction from Hiranandani — urban_hirp_addr_block() is * already a function and the pattern is identical: a category accordion * beside a static uploaded map with a click-placed pin. * * NO MAP INTEGRATION. No API, no key, no billing, no third-party script. The * pin is two percentages on an image, and the button hands off to Google Maps * — which opens the app on a phone, exactly as the YouTube link does. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_address' ) ) { function up_render_address( array $d ) { $groups = array_values( array_filter( (array) ( $d['groups'] ?? array() ), function ( $g ) { return ! empty( $g['places'] ); } ) ); $map_id = (int) ( $d['map_id'] ?? 0 ); $maps_url = (string) ( $d['maps_url'] ?? '' ); if ( ! $groups && ! $map_id ) { return; } up_wrap_css(); up_css( 'address', '.up-addr{display:grid;grid-template-columns:1fr 1.15fr;gap:32px;align-items:start}' . '.up-acc details{border-bottom:1px solid var(--up-line-soft)}' . '.up-acc summary{cursor:pointer;padding:14px 0;font-weight:600;list-style:none;' . 'display:flex;justify-content:space-between;align-items:center;font-size:14.5px}' . '.up-acc summary::-webkit-details-marker{display:none}' . '.up-acc summary::after{content:"+";color:var(--up-accent);font-size:18px;font-weight:400}' . '.up-acc details[open] summary::after{content:"–"}' . '.up-acc ul{margin:0 0 14px;padding:0;list-style:none}' . '.up-acc li{display:flex;justify-content:space-between;gap:16px;padding:7px 0;' . 'color:var(--up-mute);font-size:14px}' . '.up-acc li span{color:var(--up-mute);opacity:.75}' . '.up-acc li em{font-style:normal;color:var(--up-verified);font-weight:600}' . '.up-map{border-radius:var(--up-radius-md);overflow:hidden;box-shadow:var(--up-shadow-sm);' . 'background:var(--up-surface)}' . '.up-map-img{position:relative;line-height:0}' . '.up-map-img img{width:100%;height:auto;display:block}' . '.up-pin{position:absolute;width:16px;height:16px;border-radius:50%;background:var(--up-accent);' . 'border:3.5px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.35);transform:translate(-50%,-50%)}' . '.up-maplink{display:flex;align-items:center;justify-content:center;gap:10px;padding:14px 18px;' . 'text-decoration:none;font-size:14.5px;font-weight:600;background:#34A853;color:#fff}' . '.up-maplink:hover{filter:brightness(.94)}' . '@media(max-width:900px){.up-addr{grid-template-columns:1fr;gap:22px}}' ); echo '
'; echo '
'; foreach ( $groups as $i => $g ) { echo ''; echo '' . esc_html( $g['title'] ?? '' ) . '
    '; foreach ( (array) $g['places'] as $p ) { $name = trim( (string) ( $p['name'] ?? '' ) ); if ( '' === $name ) { continue; } echo '
  • ' . esc_html( $name ); if ( ! empty( $p['within'] ) ) { echo 'Within the development'; } elseif ( ! empty( $p['distance'] ) ) { echo '' . esc_html( $p['distance'] ) . ''; } echo '
  • '; } echo '
'; } echo '
'; echo '
'; if ( $map_id ) { $url = wp_get_attachment_image_url( $map_id, 'large' ); if ( $url ) { echo '
Locality map'; $pin = (array) ( $d['pin'] ?? array() ); if ( isset( $pin['x'], $pin['y'] ) && '' !== $pin['x'] ) { echo ''; } echo '
'; } } if ( '' !== $maps_url ) { echo '' . '' . '' . 'Open in Google Maps'; } echo '
'; echo '
'; } } /* ═══ 14. MEDIA LINKS ═════════════════════════════════════════════════ * YouTube and Instagram, in their own brand colours so they read as the * platforms they are. No video is ever hosted here — these hand off, and on * a phone the OS opens the app. Absent URL, absent button. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_media_links' ) ) { function up_render_media_links( $youtube = '', $instagram = '' ) { $youtube = trim( (string) $youtube ); $instagram = trim( (string) $instagram ); if ( '' === $youtube && '' === $instagram ) { return; } up_wrap_css(); up_css( 'medialinks', '.up-media{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}' . '.up-media a{display:flex;align-items:center;justify-content:center;gap:9px;padding:13px;' . 'border-radius:var(--up-radius-pill);text-decoration:none;font-weight:600;font-size:14.5px;color:#fff}' . '.up-media .yt{background:#FF0000}' . '.up-media .ig{background:linear-gradient(45deg,#F09433,#E6683C 25%,#DC2743 50%,#CC2366 75%,#BC1888)}' . '.up-media a:hover{filter:brightness(1.07)}' ); echo '
'; if ( '' !== $youtube ) { echo '' . '' . 'Watch the video tour'; } if ( '' !== $instagram ) { echo '' . '' . '' . '' . 'See the reel'; } echo '
'; } } /* ═══ 15. GATE ════════════════════════════════════════════════════════ * A blurred asset behind an enquiry. The library owns the MECHANICS — * blur, veil, lock chip, the data-intent hook. Whether the visitor is * unlocked, and what happens when they ask, belongs to the site: the lead * post type differs per site and the library must never learn one. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_gate' ) ) { function up_render_gate( array $d ) { $image_id = (int) ( $d['image_id'] ?? 0 ); if ( ! $image_id ) { return; } up_wrap_css(); up_css( 'gate', '.up-gate{position:relative;border-radius:var(--up-radius-md);overflow:hidden;' . 'background:var(--up-surface-warm)}' . '.up-gate img{width:100%;height:auto;display:block}' . '.up-gate.locked img{filter:blur(9px);transform:scale(1.07)}' . '.up-gate-veil{position:absolute;inset:0;display:grid;place-items:center;' . 'background:rgba(250,247,242,.55)}' . '.up-gate-chip{background:var(--up-accent);color:var(--up-on-accent);border:0;cursor:pointer;' . 'border-radius:var(--up-radius-pill);padding:13px 26px;font:600 15px var(--up-font-body);' . 'display:flex;align-items:center;gap:9px;box-shadow:var(--up-shadow-md)}' . '.up-gate-chip:hover{filter:brightness(.95)}' ); $locked = empty( $d['unlocked'] ); $url = wp_get_attachment_image_url( $image_id, 'large' ); if ( ! $url ) { return; } echo '
'; echo '' . esc_attr( $d['alt'] ?? '' ) . ''; if ( $locked ) { echo '
'; } echo '
'; } } /* ═══ 16. LISTING CARDS ═══════════════════════════════════════════════ * Related listings and archive results. Distinct from up_render_project_cards: * a project card carries possession and "onwards" pricing, a listing card * carries a firm price, carpet and floor. Different purchase, different facts. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_listing_cards' ) ) { function up_render_listing_cards( array $cards, array $head = array() ) { $cards = array_values( array_filter( $cards, function ( $c ) { return '' !== trim( (string) ( $c['title'] ?? '' ) ); } ) ); if ( ! $cards ) { return; } up_wrap_css(); up_css( 'listcards', '.up-lcards{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}' . '.up-lcard{background:var(--up-surface);border-radius:var(--up-radius-md);overflow:hidden;' . 'box-shadow:var(--up-shadow-sm);text-decoration:none;display:block;' . 'transition:transform .2s ease,box-shadow .2s ease}' . '.up-lcard:hover{box-shadow:var(--up-shadow-md);transform:translateY(-3px)}' . '.up-lcard-im{position:relative}' . '.up-lcard img{width:100%;height:160px;object-fit:cover;display:block}' . '.up-lcard-price{position:absolute;left:11px;bottom:11px;background:var(--up-surface);' . 'border-radius:var(--up-radius-sm);padding:6px 12px;font-weight:700;font-size:15.5px;' . 'box-shadow:var(--up-shadow-sm)}' . '.up-lcard-tag{position:absolute;left:11px;top:11px;font-size:11.5px;font-weight:600;' . 'padding:5px 11px;border-radius:var(--up-radius-pill);' . 'background:var(--up-soft-green);color:var(--up-verified)}' . '.up-lcard-b{padding:14px 16px}' . '.up-lcard-b h3{font-family:var(--up-font-display);font-size:16.5px;font-weight:600;' . 'margin:0;line-height:1.4;color:var(--up-ink)}' . '.up-lcard-loc{font-size:13.5px;color:var(--up-mute);margin:3px 0 8px}' . '.up-lcard-meta{font-size:13px;color:var(--up-mute);opacity:.85}' . '@media(max-width:900px){.up-lcards{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;' . 'scrollbar-width:none;margin:0 -16px;padding:0 16px 4px}' . '.up-lcards::-webkit-scrollbar{display:none}' . '.up-lcard{flex:none;width:250px;scroll-snap-align:start}}' ); if ( ! empty( $head['heading'] ) ) { echo '
'; if ( ! empty( $head['eyebrow'] ) ) { echo '' . esc_html( $head['eyebrow'] ) . ''; } echo '

' . wp_kses( $head['heading'], array( 'em' => array() ) ) . '

'; } echo '
'; foreach ( $cards as $c ) { echo ''; echo '
'; if ( ! empty( $c['image'] ) ) { echo ''; } if ( ! empty( $c['tag'] ) ) { echo '' . esc_html( $c['tag'] ) . ''; } if ( ! empty( $c['price'] ) ) { echo '' . esc_html( $c['price'] ) . ''; } echo '
'; echo '

' . esc_html( $c['title'] ) . '

'; if ( ! empty( $c['location'] ) ) { echo '
' . esc_html( $c['location'] ) . '
'; } if ( ! empty( $c['meta'] ) ) { echo '
' . esc_html( $c['meta'] ) . '
'; } echo '
'; } echo '
'; } } /* ═══ 17. PROSE ═══════════════════════════════════════════════════════ * A description. Paragraph breaks on blank lines, escaped throughout — * submitters type this and it must never carry markup onto the page. * ═════════════════════════════════════════════════════════════════════ */ if ( ! function_exists( 'up_render_prose' ) ) { function up_render_prose( $text, $max_ch = 70 ) { $text = trim( (string) $text ); if ( '' === $text ) { return; } up_wrap_css(); up_css( 'prose', '.up-prose{color:var(--up-mute);font-size:16px;line-height:1.7}' . '.up-prose p{margin:0 0 13px}' . '.up-prose p:last-child{margin-bottom:0}' ); echo '
'; foreach ( preg_split( '/\n\s*\n/', $text ) as $para ) { $para = trim( $para ); if ( '' !== $para ) { echo '

' . nl2br( esc_html( $para ) ) . '

'; } } echo '
'; } } Page Not Found | Raymond Realty | Urban Properties Page Not Found | urbanproperties.co.in
Enquire Now
404
Address Not Found

This address doesn't exist

The page you're looking for isn't part of the Raymond Realty portfolio on Urban Properties. It may have moved, been retired, or never existed. Explore our live inventory below — or return to the Raymond home.

Authorised Sales Partner: Urban Properties is an Authorised Sales Partner for Raymond Realty projects. All information subject to MahaRERA disclosure and developer confirmation. Official: raymondrealty.in
ENQUIRE