PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-seo-suite.php

class-metasync-seo-suite.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at includes/class-metasync-seo-suite.php

1,161 lines 79.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Unified "SEO Suite" meta box.
4 *
5 * Consolidates MetaSync's separate Classic-editor meta boxes (SEO, Robots,
6 * Canonical, Redirection, Social & Open Graph, Schema, Video Sitemap) into a
7 * single tabbed box. Classic editor only — the block editor keeps its own SEO
8 * sidebar and is left untouched.
9 *
10 * Presentation-only merge: this class does NOT reimplement any save logic. It
11 * runs after MetaSync registers its own boxes, captures their render callbacks,
12 * removes them from display, and renders the same fields (identical name/nonce
13 * attributes) inside one tabbed container. The plugin's existing save_post
14 * handlers persist everything unchanged. Schema is rendered via MetaSync's own
15 * builder callback. Custom HTML settings stay in their standalone box.
16 *
17 * On LPS / custom-HTML pages (metasync_is_custom_or_lps_page) the box shows a
18 * read-only notice and emits no nonces, so nothing here overwrites the
19 * externally-managed SEO for those pages.
20 *
21 * @package Metasync
22 */
23
24 if (!defined('ABSPATH')) {
25 exit;
26 }
27
28 class Metasync_Seo_Suite
29 {
30 /** Original MetaSync meta-box IDs this box absorbs (Custom HTML is intentionally excluded). */
31 private $target_ids = array(
32 'metasync-seo-meta',
33 'metasync-seo-lps-notice',
34 'common-robots-meta',
35 'advance-robots-meta',
36 'post-canonical-meta',
37 'post-redirection-meta',
38 'metasync_opengraph_meta_box',
39 'metasync-schema-markup',
40 'metasync-video-sitemap-meta',
41 );
42
43 /** @var array<string,bool> Which target boxes were present on the current screen. */
44 private $present = array();
45
46 /** @var array|null Captured Schema meta-box definition (rendered inside the Schema tab). */
47 private $schema_box = null;
48
49 public function __construct()
50 {
51 add_action('add_meta_boxes', array($this, 'unify'), 9999, 2);
52 add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
53 }
54
55 /** Load the media library for the OG/Twitter image pickers. */
56 public function enqueue_assets($hook)
57 {
58 if (in_array($hook, array('post.php', 'post-new.php'), true)) {
59 wp_enqueue_media();
60 }
61 }
62
63 /**
64 * True for any request originating from the block editor.
65 *
66 * The block editor renders its meta-box area during the main page load
67 * (is_block_editor() is true there) and re-posts that same form to
68 * post.php?meta-box-loader=1 when saving. Both are treated as block-editor
69 * context so this box never engages in either.
70 */
71 private function is_block_editor_request()
72 {
73 if (!empty($_REQUEST['meta-box-loader'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request flag for meta box registration
74 return true;
75 }
76 $screen = get_current_screen();
77 return $screen && $screen->is_block_editor();
78 }
79
80 /**
81 * Runs after MetaSync registers its boxes (default priority). Captures the
82 * target boxes, removes them from display, and registers the unified box.
83 */
84 public function unify($post_type, $post)
85 {
86 /** Allow sites to opt out of the unified box and keep the classic separate boxes. */
87 if (!apply_filters('metasync_enable_seo_suite', true, $post_type, $post)) {
88 return;
89 }
90
91 // Classic editor only. In the block editor the plugin's own SEO sidebar owns
92 // these fields, and the SEO box is registered __back_compat_meta_box so it
93 // never renders there at all. Returning before any remove_meta_box() call
94 // leaves the block editor byte-for-byte as it is without this class.
95 if ($this->is_block_editor_request()) {
96 return;
97 }
98
99 global $wp_meta_boxes;
100 if (empty($wp_meta_boxes[$post_type])) {
101 return;
102 }
103
104 $this->present = array();
105 $this->schema_box = null;
106
107 foreach ($wp_meta_boxes[$post_type] as $context => $priorities) {
108 foreach ((array) $priorities as $boxes) {
109 if (!is_array($boxes)) {
110 continue;
111 }
112 foreach ($boxes as $box_id => $box) {
113 if (in_array($box_id, $this->target_ids, true) && $box && !empty($box['callback'])) {
114 $this->present[$box_id] = true;
115 if ($box_id === 'metasync-schema-markup') {
116 $this->schema_box = $box;
117 }
118 }
119 }
120 }
121 foreach ($this->target_ids as $id) {
122 remove_meta_box($id, $post_type, $context);
123 }
124 }
125
126 if (empty($this->present)) {
127 return;
128 }
129
130 if (empty($wp_meta_boxes[$post_type]['normal']['high']['metasync_seo_suite'])) {
131 $plugin_name = class_exists('Metasync') ? Metasync::get_effective_plugin_name() : 'MetaSync';
132 add_meta_box(
133 'metasync_seo_suite',
134 sprintf('SEO Suite by %s', $plugin_name),
135 array($this, 'render'),
136 $post_type,
137 'normal',
138 'high'
139 );
140 }
141 }
142
143 /** True when the page's front-end output bypasses WordPress SEO (LPS / custom-HTML). */
144 private function is_managed_page($post_id)
145 {
146 if (function_exists('metasync_is_custom_or_lps_page') && $post_id > 0 && metasync_is_custom_or_lps_page($post_id)) {
147 return true;
148 }
149 // Fallbacks if the helper isn't loaded on this request.
150 if (get_post_meta($post_id, '_metasync_lps_import', true)) {
151 return true;
152 }
153 if (get_post_meta($post_id, '_metasync_is_custom_html_page', true) === '1'
154 && get_post_meta($post_id, '_metasync_raw_html_enabled', true) === '1') {
155 return true;
156 }
157 return false;
158 }
159
160 private static function v($post_id, $key)
161 {
162 $value = get_post_meta($post_id, $key, true);
163 if (is_array($value)) {
164 return '';
165 }
166 // Collapse the "Auto Draft" placeholder the meta box pre-fill can persist into
167 // the social title/description keys, so the unified box shows the real pre-fill
168 // instead and re-saving the post clears the stored placeholder.
169 // @phpstan-ignore-next-line function.alreadyNarrowedType
170 if (method_exists('Metasync_OpenGraph', 'strip_auto_draft_title')
171 && defined('Metasync_OpenGraph::AUTO_DRAFT_PRONE_KEYS')
172 && in_array($key, Metasync_OpenGraph::AUTO_DRAFT_PRONE_KEYS, true)
173 ) {
174 $value = Metasync_OpenGraph::strip_auto_draft_title($value);
175 }
176 // A social title that merely mirrors the post title is the old pre-fill's
177 // snapshot — stale the moment the post is renamed — not a customization.
178 // Collapsed so the field renders empty (the default stays visible as the
179 // placeholder) and the shared save handler stores nothing for it.
180 // @phpstan-ignore-next-line function.alreadyNarrowedType
181 if (method_exists('Metasync_OpenGraph', 'strip_title_snapshot')
182 && defined('Metasync_OpenGraph::TITLE_DEFAULTED_KEYS')
183 && in_array($key, Metasync_OpenGraph::TITLE_DEFAULTED_KEYS, true)
184 && is_string($value)
185 ) {
186 $value = Metasync_OpenGraph::strip_title_snapshot($post_id, $key, $value);
187 }
188 // A social description that merely mirrors the resolved excerpt is the
189 // same kind of snapshot — stale the moment the excerpt or content
190 // changes — collapsed for the same reason as the title above.
191 // @phpstan-ignore-next-line function.alreadyNarrowedType
192 if (method_exists('Metasync_OpenGraph', 'strip_description_snapshot')
193 && defined('Metasync_OpenGraph::DESCRIPTION_DEFAULTED_KEYS')
194 && in_array($key, Metasync_OpenGraph::DESCRIPTION_DEFAULTED_KEYS, true)
195 && is_string($value)
196 ) {
197 $value = Metasync_OpenGraph::strip_description_snapshot($post_id, $key, $value);
198 }
199 return (string) $value;
200 }
201
202 private static function checked_attr($cond)
203 {
204 return $cond ? ' checked' : '';
205 }
206
207 /** A clean, "pretty" permalink for placeholders/preview (avoids ?p=ID on drafts). */
208 private static function pretty_permalink($post)
209 {
210 $link = get_permalink($post->ID);
211 if ($link
212 && strpos($link, '?p=') === false
213 && strpos($link, '?page_id=') === false
214 && strpos($link, 'preview=') === false) {
215 return $link;
216 }
217 $slug = $post->post_name ? $post->post_name : sanitize_title(get_the_title($post->ID));
218 if (!$slug) {
219 $slug = 'your-post';
220 }
221 return user_trailingslashit(home_url('/' . $slug . '/'));
222 }
223
224 /**
225 * Render the unified box.
226 */
227 public function render($post)
228 {
229 $P = $this->present;
230 $id = $post->ID;
231
232 $managed = $this->is_managed_page($id);
233
234 $has_seo = isset($P['metasync-seo-meta']) || isset($P['metasync-seo-lps-notice']);
235 $has_crob = isset($P['common-robots-meta']);
236 $has_arob = isset($P['advance-robots-meta']);
237 $has_robots = $has_crob || $has_arob;
238 $has_canon = isset($P['post-canonical-meta']);
239 $has_redir = isset($P['post-redirection-meta']);
240 $has_social = isset($P['metasync_opengraph_meta_box']);
241 $has_schema = isset($P['metasync-schema-markup']);
242 $has_video = isset($P['metasync-video-sitemap-meta']);
243
244 // Breadcrumb Title is useful while MetaSync breadcrumbs are enabled or
245 // while Yoast/Rank Math owns the rendered breadcrumb trail.
246 $has_breadcrumbs = class_exists('Metasync_Breadcrumbs')
247 ? Metasync_Breadcrumbs::is_breadcrumb_title_meaningful()
248 : true;
249
250 // The Language Alternates tab follows its own editor-settings switch,
251 // not the Canonical metabox's — disabling one must not hide the other.
252 // The defined() guard matches the REST registration's defence against
253 // an opcache-stale copy of the flags class during an upgrade window.
254 // @phpstan-ignore-next-line function.alreadyNarrowedType
255 $has_lang = defined('Metasync_Feature_Flags::LANGUAGE_ALTERNATES')
256 && Metasync_Feature_Flags::is_enabled(Metasync_Feature_Flags::LANGUAGE_ALTERNATES);
257
258 // Current values.
259 $seo_title = self::v($id, '_metasync_seo_title');
260 $seo_desc = self::v($id, '_metasync_seo_desc');
261
262 // The read-only "OTTO keyword" row shows OTTO's suggestion under a lock
263 // icon. Which tier that is — and whether OTTO counts at all, since the
264 // per-post "Disable OTTO" toggle makes its stored suggestion dead data —
265 // is the resolver's call, not this screen's. fallback() drops the
266 // customer tier and honours Disable OTTO, so an OTTO-disabled post stops
267 // showing a dead suggestion here.
268 $kw_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_FOCUS_KEYWORD);
269 $otto_kw = $kw_fallback['value'];
270
271 // Classic-editor fields sharing meta keys with the Gutenberg sidebar,
272 // so values round-trip between editors. The save path lives in
273 // Metasync_Classic_Seo_Fields (the Suite stays presentation-only).
274 $breadcrumb_title = self::v($id, '_metasync_breadcrumb_title');
275
276 // hreflang is stored as a JSON string of {lang, region, url} rows.
277 // Decode defensively — malformed, empty, and non-array stored values
278 // must not fatal — the same pattern
279 // Metasync_Hreflang_Output::get_manual_entries() already uses. Values
280 // render as stored, like the Gutenberg panel, which warns about (but
281 // keeps) rows that fail validation.
282 $hreflang_rows = array();
283 $raw_hreflang = get_post_meta($id, '_metasync_hreflang', true);
284 if (is_string($raw_hreflang) && $raw_hreflang !== '') {
285 $decoded = json_decode($raw_hreflang, true);
286 if (is_array($decoded)) {
287 foreach ($decoded as $entry) {
288 if (!is_array($entry)) {
289 continue;
290 }
291 $hreflang_rows[] = array(
292 'lang' => isset($entry['lang']) ? (string) $entry['lang'] : '',
293 'region' => isset($entry['region']) ? (string) $entry['region'] : '',
294 'url' => isset($entry['url']) ? (string) $entry['url'] : '',
295 );
296 }
297 }
298 }
299
300 // The greyed-out value each field shows when left blank: OTTO's
301 // suggestion, or the imported value where OTTO is silent. Which tier
302 // that is — and whether OTTO counts at all, since the per-post "Disable
303 // OTTO" toggle makes its stored suggestion dead data — is
304 // Metasync_Seo_Precedence's call, not this screen's. Without a ghost the
305 // fields look completely empty after an import and the customer
306 // concludes it did nothing.
307 $title_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_TITLE);
308 $desc_fallback = Metasync_Seo_Precedence::fallback($id, Metasync_Seo_Precedence::FIELD_DESCRIPTION);
309
310 $ghost_title = $title_fallback['value'];
311 $ghost_desc = $desc_fallback['value'];
312
313 $show_imported_note = $title_fallback['source'] === Metasync_Seo_Precedence::SOURCE_IMPORTED
314 || $desc_fallback['source'] === Metasync_Seo_Precedence::SOURCE_IMPORTED;
315
316 // Robots: post meta first, falling back to the site-wide defaults exactly as
317 // the standalone Common/Advance Robots boxes do (new option spelling first,
318 // then the legacy "mata" spelling).
319 $cr = get_post_meta($id, 'metasync_common_robots', true);
320 if (!$cr && class_exists('Metasync')) {
321 $cr = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
322 }
323 if (!is_array($cr)) {
324 $cr = array();
325 }
326 $ar = get_post_meta($id, 'metasync_advance_robots', true);
327 if (!$ar && class_exists('Metasync')) {
328 $ar = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
329 }
330 if (!is_array($ar)) {
331 $ar = array();
332 }
333 $ar_snip_e = isset($ar['max-snippet']['enable']) ? $ar['max-snippet']['enable'] : '';
334 $ar_snip_l = isset($ar['max-snippet']['length']) ? $ar['max-snippet']['length'] : '';
335 $ar_vid_e = isset($ar['max-video-preview']['enable']) ? $ar['max-video-preview']['enable'] : '';
336 $ar_vid_l = isset($ar['max-video-preview']['length']) ? $ar['max-video-preview']['length'] : '';
337 $ar_img_e = isset($ar['max-image-preview']['enable']) ? $ar['max-image-preview']['enable'] : '';
338 $ar_img_l = isset($ar['max-image-preview']['length']) ? $ar['max-image-preview']['length'] : '';
339
340 // Canonical: same sanitise-and-repair path as the standalone Canonical box.
341 // A bare reset() here would re-corrupt nested-array rows to the string "Array".
342 $raw_canonical = get_post_meta($id, 'meta_canonical', true);
343 if (class_exists('Metasync_Canonical_Sanitizer')) {
344 $canon = Metasync_Canonical_Sanitizer::sanitize($raw_canonical);
345 if (is_array($raw_canonical) || Metasync_Canonical_Sanitizer::is_corrupted($raw_canonical)) {
346 if ($canon !== '') {
347 update_post_meta($id, 'meta_canonical', $canon);
348 } else {
349 delete_post_meta($id, 'meta_canonical');
350 }
351 } elseif ($canon === '' && is_string($raw_canonical) && trim($raw_canonical) !== '') {
352 // Stored value the validator doesn't recognise: show it as-is so a
353 // routine post save doesn't silently wipe it.
354 $canon = trim($raw_canonical);
355 }
356 } else {
357 $canon = is_array($raw_canonical) ? '' : (string) $raw_canonical;
358 }
359 $pretty = self::pretty_permalink($post);
360
361 $rd = get_post_meta($id, 'metasync_post_redirection_meta', true);
362 if (!is_array($rd)) {
363 $rd = array();
364 }
365 $rd_en = (isset($rd['enable']) ? $rd['enable'] : '') === 'true';
366 $rd_type = isset($rd['type']) ? $rd['type'] : '301';
367 $rd_url = isset($rd['url']) ? $rd['url'] : '';
368
369 // The standalone Open Graph box defaults this setting to enabled when
370 // no value has been saved yet. Preserve that behaviour: only an
371 // explicit '0' represents an editor opting out.
372 $og_en_value = get_post_meta($id, '_metasync_og_enabled', true);
373 $og_en = ($og_en_value !== '0');
374 // Match the standalone Open Graph box exactly: its computed defaults are
375 // real form values (and are therefore persisted on save), not placeholders.
376 $og_defaults = array('title' => '', 'description' => '', 'image' => '');
377 $opengraph = class_exists('Metasync_OpenGraph') ? Metasync_OpenGraph::get_instance() : null;
378 if ($opengraph) {
379 $og_defaults = $opengraph->get_default_og_values($id);
380 }
381 // Social titles and descriptions render placeholder-only: the default
382 // (the post title, the resolved excerpt) stays visible in the
383 // placeholder but is never a form value, so submitting the page cannot
384 // persist a snapshot of it. Images keep their computed default as a
385 // real value, matching the standalone OG box.
386 $og_title = self::v($id, '_metasync_og_title');
387 $og_desc = self::v($id, '_metasync_og_description');
388 $og_image = self::v($id, '_metasync_og_image') ?: $og_defaults['image'];
389 $og_type = self::v($id, '_metasync_og_type') ?: 'article';
390 $og_url = self::v($id, '_metasync_og_url');
391 if ($og_url === '') {
392 $og_url = $opengraph ? $opengraph->get_canonical_url($post) : $pretty;
393 }
394 $tw_card = self::v($id, '_metasync_twitter_card') ?: 'summary_large_image';
395 $tw_site = self::v($id, '_metasync_twitter_site');
396 $tw_title = self::v($id, '_metasync_twitter_title');
397 // Placeholder-only like the standalone box: when no twitter title is
398 // stored the placeholder shows the og title if one is set, else the
399 // default (post) title — the exact chain the standalone OG box uses.
400 $tw_title_placeholder = ($og_title !== '') ? $og_title : $og_defaults['title'];
401 // Same chain for the twitter description's placeholder: the og
402 // description if one is set, else the resolved excerpt default.
403 $tw_desc_placeholder = ($og_desc !== '') ? $og_desc : $og_defaults['description'];
404 $tw_desc = self::v($id, '_metasync_twitter_description');
405 $tw_image = self::v($id, '_metasync_twitter_image') ?: $og_image;
406 $tw_alt = self::v($id, '_metasync_twitter_image_alt');
407 $tw_app_iphone = self::v($id, '_metasync_twitter_app_id_iphone');
408 $tw_app_ipad = self::v($id, '_metasync_twitter_app_id_ipad');
409 $tw_app_gp = self::v($id, '_metasync_twitter_app_id_googleplay');
410 $tw_app_url_iphone = self::v($id, '_metasync_twitter_app_url_iphone');
411 $tw_app_url_ipad = self::v($id, '_metasync_twitter_app_url_ipad');
412 $tw_app_url_gp = self::v($id, '_metasync_twitter_app_url_googleplay');
413 $tw_app_country = self::v($id, '_metasync_twitter_app_country');
414 $tw_player = self::v($id, '_metasync_twitter_player');
415 $tw_player_w = self::v($id, '_metasync_twitter_player_width');
416 $tw_player_h = self::v($id, '_metasync_twitter_player_height');
417 $tw_app_disabled = $tw_card === 'app' ? '' : ' disabled';
418 $tw_player_disabled = $tw_card === 'player' ? '' : ' disabled';
419
420 $v_url = self::v($id, '_metasync_video_url');
421 $v_thumb = self::v($id, '_metasync_video_thumbnail');
422 $v_title = self::v($id, '_metasync_video_title');
423 $v_desc = self::v($id, '_metasync_video_description');
424 $v_dur = self::v($id, '_metasync_video_duration');
425
426 $host = strtoupper((string) wp_parse_url(home_url(), PHP_URL_HOST));
427 $host_lc = strtolower($host);
428 $prev_url_disp = preg_replace('#^https?://#', '', (string) $pretty);
429 $site = get_bloginfo('name');
430 if ($site === '') {
431 $site = $host;
432 }
433 $avatar = strtoupper(function_exists('mb_substr') ? mb_substr($site, 0, 1) : substr($site, 0, 1));
434 $handle = sanitize_title($site) ?: 'site';
435
436 $this->styles();
437 ?>
438 <div id="mss-root" data-theme="light"<?php echo $managed ? ' class="lps"' : ''; ?>>
439 <?php
440 // On managed pages (LPS / custom-HTML) we emit NO nonces, so nothing here can
441 // overwrite the externally-managed SEO for those pages.
442 if (!$managed) {
443 if ($has_seo) { wp_nonce_field('metasync_seo_meta_nonce', 'metasync_seo_meta_nonce'); }
444 if ($has_crob) { wp_nonce_field('metasync_common_robots_nonce', 'metasync_common_robots_nonce'); }
445 if ($has_arob) { wp_nonce_field('metasync_advance_robots_nonce', 'metasync_advance_robots_nonce'); }
446 if ($has_canon) { wp_nonce_field('metasync_post_canonical_nonce', 'metasync_post_canonical_nonce'); }
447 if ($has_redir) { wp_nonce_field('metasync_post_redirection_nonce', 'metasync_post_redirection_nonce'); }
448 if ($has_social) { wp_nonce_field('metasync_opengraph_nonce', 'metasync_opengraph_nonce'); }
449 if ($has_video) { wp_nonce_field('metasync_video_sitemap_meta_nonce', 'metasync_video_sitemap_meta_nonce'); }
450 // Classic-editor-only fields: emit the nonce only when at least
451 // one of them actually renders (breadcrumb title in the SEO tab,
452 // Languages tab). Metasync_Classic_Seo_Fields verifies it.
453 if (($has_seo && $has_breadcrumbs) || $has_lang) {
454 wp_nonce_field('metasync_classic_seo_fields_nonce', 'metasync_classic_seo_fields_nonce');
455 }
456 }
457 ?>
458 <div class="metabox">
459 <div class="mb-head">
460 <div class="badge">M</div>
461 <div class="t"><b>SEO Suite</b><span>On-page SEO for this post</span></div>
462 <div class="mss-actions">
463 <button type="button" class="theme-toggle" onclick="mssTheme(this)"><span class="ti">&#127769;</span> <span class="tl">Dark</span></button>
464 </div>
465 </div>
466
467 <div class="mss-lockable">
468 <div class="mb-body"<?php echo $managed ? ' inert' : ''; ?>>
469 <div class="rail">
470 <?php
471 $first = true;
472 $tab = function ($key, $icon, $label) use (&$first) {
473 echo '<div class="nav' . ($first ? ' active' : '') . '" role="button" tabindex="0" onclick="mssGo(this,\'' . esc_attr($key) . '\')" onkeydown="mssKey(event,this)"><span class="ic">' . esc_html($icon) . '</span> ' . esc_html($label) . '</div>';
474 $first = false;
475 };
476 if ($has_seo) { $tab('seo', '&#128269;', 'SEO'); }
477 if ($has_robots) { $tab('robots', '&#129302;', 'Robots'); }
478 if ($has_canon) { $tab('canonical', '&#128279;', 'Canonical'); }
479 if ($has_lang) { $tab('languages', '&#127760;', 'Languages'); }
480 if ($has_redir) { $tab('redirect', '&#8618;&#65039;', 'Redirection'); }
481 if ($has_social) { $tab('social', '&#128226;', 'Social &amp; OG'); }
482 if ($has_schema) { $tab('schema', '&#128208;', 'Schema'); }
483 if ($has_video) { $tab('video', '&#127916;', 'Video'); }
484 ?>
485 </div>
486
487 <div class="panel-wrap">
488 <?php
489 $pfirst = true;
490 $pcls = function () use (&$pfirst) {
491 $c = 'panel' . ($pfirst ? ' active' : '');
492 $pfirst = false;
493 return $c;
494 };
495 ?>
496
497 <?php if ($has_seo): ?>
498 <div class="<?php echo esc_attr($pcls()); ?>" data-p="seo">
499 <h2 class="p-title">&#128269; Search Appearance</h2>
500 <p class="p-sub">Set the SEO Title and Meta Description. Leave blank to use the OTTO suggestion.</p>
501 <?php if ($show_imported_note): ?>
502 <p class="p-sub">The greyed-out value below was imported from another SEO plugin. It is used only until OTTO has a suggestion for this page.</p>
503 <?php endif; ?>
504 <div class="field">
505 <div class="lbl"><span>SEO Title</span><span class="cc"><span class="tC">0</span>/60</span></div>
506 <input class="ctrl seoT" name="metasync_seo_title" value="<?php echo esc_attr($seo_title); ?>" placeholder="<?php echo esc_attr($ghost_title); ?>" oninput="mssRc()">
507 </div>
508 <div class="field">
509 <div class="lbl"><span>Meta Description</span><span class="cc"><span class="dC">0</span>/160</span></div>
510 <textarea class="ctrl seoD" name="metasync_seo_desc" placeholder="<?php echo esc_attr($ghost_desc); ?>" oninput="mssRc()"><?php echo esc_textarea($seo_desc); ?></textarea>
511 </div>
512 <?php if ($has_breadcrumbs): ?>
513 <div class="field">
514 <div class="lbl"><span>Breadcrumb Title</span><span class="mss-tip" title="The label shown for this post in breadcrumb trails. Leave blank to use the post title.">?</span></div>
515 <input class="ctrl" name="_metasync_breadcrumb_title" value="<?php echo esc_attr($breadcrumb_title); ?>" placeholder="Defaults to the post title">
516 <div class="hint">Overrides the breadcrumb label for this post. Leave blank to use the post title.</div>
517 </div>
518 <?php endif; ?>
519 <?php if ($otto_kw !== ''): ?>
520 <div class="field">
521 <div class="lbl"><span>Focus Keyword</span><span class="ro-badge">Managed by OTTO</span></div>
522 <div class="mss-ro"><span class="lock">&#128274;</span> <?php echo esc_html($otto_kw); ?></div>
523 <div class="hint">Set in Search Atlas &mdash; shown here for reference. Read-only.</div>
524 </div>
525 <?php endif; ?>
526 <div class="sec">Preview</div>
527 <div class="gprev">
528 <div class="u"><?php echo esc_html($prev_url_disp); ?></div>
529 <div class="ti gT"><?php echo esc_html($seo_title ?: ($ghost_title ?: get_the_title($id))); ?></div>
530 <div class="de gD"><?php echo esc_html($seo_desc ?: ($ghost_desc ?: 'No description set.')); ?></div>
531 </div>
532 </div>
533 <?php endif; ?>
534
535 <?php if ($has_robots): ?>
536 <div class="<?php echo esc_attr($pcls()); ?>" data-p="robots">
537 <h2 class="p-title">&#129302; Robots Meta</h2>
538 <p class="p-sub">Crawler directives for this post. These are independent flags &mdash; combine as needed.</p>
539 <?php if ($has_crob): ?>
540 <div class="sec">Common directives</div>
541 <div class="chips">
542 <?php
543 $common = array(
544 'index' => array('Index', 'Allow search engines to index this page'),
545 'noindex' => array('No Index', 'Keep this page out of results'),
546 'nofollow' => array('No Follow', "Don't follow links on this page"),
547 'noarchive' => array('No Archive', 'No cached copy'),
548 'noimageindex' => array('No Image Index', 'Exclude images from image search'),
549 'nosnippet' => array('No Snippet', 'No text snippet in results'),
550 );
551 foreach ($common as $key => $c): ?>
552 <label class="chip">
553 <input type="checkbox" name="common_robots_meta[<?php echo esc_attr($key); ?>]" value="<?php echo esc_attr($key); ?>"<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr() returns the validated checked attribute.
554 echo isset($cr[$key]) ? esc_attr(self::checked_attr($cr[$key] === $key)) : ''; ?>>
555 <span class="box">&#10003;</span><div><?php echo esc_html($c[0]); ?><small><?php echo esc_html($c[1]); ?></small></div>
556 </label>
557 <?php endforeach; ?>
558 </div>
559 <?php endif; ?>
560 <?php if ($has_arob): ?>
561 <div class="sec">Advanced limits <span class="sec-note">&mdash; tick a limit to apply it</span></div>
562 <div class="row">
563 <div class="field">
564 <label class="chip mss-advchip"><input type="checkbox" name="advanced_robots_meta[max-snippet][enable]" value="1"<?php checked('1', $ar_snip_e); ?>><span class="box">&#10003;</span><div>Max Snippet</div></label>
565 <input class="ctrl" type="number" name="advanced_robots_meta[max-snippet][length]" value="<?php echo esc_attr($ar_snip_l); ?>" min="-1" placeholder="e.g. 160">
566 <div class="hint">Max snippet length in characters.</div></div>
567 <div class="field">
568 <label class="chip mss-advchip"><input type="checkbox" name="advanced_robots_meta[max-video-preview][enable]" value="1"<?php checked('1', $ar_vid_e); ?>><span class="box">&#10003;</span><div>Max Video Preview</div></label>
569 <input class="ctrl" type="number" name="advanced_robots_meta[max-video-preview][length]" value="<?php echo esc_attr($ar_vid_l); ?>" min="-1" placeholder="e.g. -1">
570 <div class="hint">Max preview duration in seconds.</div></div>
571 </div>
572 <div class="field">
573 <label class="chip mss-advchip"><input type="checkbox" name="advanced_robots_meta[max-image-preview][enable]" value="1"<?php checked('1', $ar_img_e); ?>><span class="box">&#10003;</span><div>Max Image Preview</div></label>
574 <select class="ctrl" name="advanced_robots_meta[max-image-preview][length]">
575 <option value="large"<?php selected($ar_img_l, 'large'); ?>>Large</option>
576 <option value="standard"<?php selected($ar_img_l, 'standard'); ?>>Standard</option>
577 <option value="none"<?php selected($ar_img_l, 'none'); ?>>None</option>
578 </select></div>
579 <?php endif; ?>
580 </div>
581 <?php endif; ?>
582
583 <?php if ($has_canon): ?>
584 <div class="<?php echo esc_attr($pcls()); ?>" data-p="canonical">
585 <h2 class="p-title">&#128279; Canonical URL</h2>
586 <p class="p-sub">Point search engines to the preferred version of this content.</p>
587 <div class="field">
588 <div class="lbl"><span>Canonical URL</span></div>
589 <input class="ctrl" type="text" name="post_canonical_url_meta" value="<?php echo esc_attr($canon); ?>" placeholder="<?php echo esc_attr($pretty); ?>">
590 <div class="hint">Leave blank to use this post's own URL.</div>
591 </div>
592 </div>
593 <?php endif; ?>
594
595 <?php if ($has_lang): ?>
596 <div class="<?php echo esc_attr($pcls()); ?>" data-p="languages">
597 <h2 class="p-title">&#127760; Language Alternates</h2>
598 <p class="p-sub">Tell search engines about translated or regional versions of this post.</p>
599 <?php
600 // Presence marker: with every row removed the rows inputs
601 // are gone from the form entirely, so without this flag the
602 // save handler could not tell "cleared" from "tab absent".
603 ?>
604 <input type="hidden" name="metasync_hreflang_present" value="1">
605 <div class="mss-hreflang-rows">
606 <?php
607 $hfi = 0;
608 foreach ($hreflang_rows as $hrow):
609 $hname = 'metasync_hreflang_rows[' . (int) $hfi . ']';
610 ?>
611 <div class="mss-hrow">
612 <div class="row">
613 <div class="field"><div class="lbl"><span>Language</span></div>
614 <input class="ctrl" name="<?php echo esc_attr($hname); ?>[lang]" value="<?php echo esc_attr($hrow['lang']); ?>" placeholder="en"></div>
615 <div class="field"><div class="lbl"><span>Region</span></div>
616 <input class="ctrl" name="<?php echo esc_attr($hname); ?>[region]" value="<?php echo esc_attr($hrow['region']); ?>" placeholder="US"></div>
617 </div>
618 <div class="field"><div class="lbl"><span>URL</span></div>
619 <div class="mss-hrow-url">
620 <input class="ctrl" type="text" name="<?php echo esc_attr($hname); ?>[url]" value="<?php echo esc_attr($hrow['url']); ?>" placeholder="https://example.com/en-us/">
621 <button type="button" class="btn ghost sm mss-hreflang-remove" onclick="mssHreflangRemove(this)">Remove</button>
622 </div></div>
623 </div>
624 <?php $hfi++; endforeach; ?>
625 </div>
626 <div class="mss-hreflang-empty"<?php echo empty($hreflang_rows) ? '' : ' style="display:none"'; ?>>
627 <p>No language alternates yet. Add one to tell search engines about a translated or regional version of this post.</p>
628 </div>
629 <button type="button" class="btn ghost mss-hreflang-add" onclick="mssHreflangAdd(this)">&#43; Add alternate</button>
630 <div class="hint">Use ISO codes (e.g. en, en-US, x-default) and absolute URLs. A self-reference is added automatically when missing.</div>
631 </div>
632 <?php endif; ?>
633
634 <?php if ($has_redir): ?>
635 <div class="<?php echo esc_attr($pcls()); ?>" data-p="redirect">
636 <h2 class="p-title">&#8618;&#65039; Redirection</h2>
637 <p class="p-sub">Send visitors of this post somewhere else.</p>
638 <div class="switch-card">
639 <label class="switch"><input type="checkbox" name="post_redirect_meta[enable]" value="true"<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr() returns the validated checked attribute.
640 echo esc_attr(self::checked_attr($rd_en)); ?>></label>
641 <div class="m"><b>Enable redirection</b><span>Send visitors of this post to another URL</span></div>
642 </div>
643 <div class="row">
644 <div class="field"><div class="lbl"><span>Type</span></div>
645 <select class="ctrl" name="post_redirect_meta[type]">
646 <option value="301"<?php selected($rd_type, '301'); ?>>301 Permanent Move</option>
647 <option value="302"<?php selected($rd_type, '302'); ?>>302 Temporary Move</option>
648 <option value="307"<?php selected($rd_type, '307'); ?>>307 Temporary Redirect</option>
649 <option value="410"<?php selected($rd_type, '410'); ?>>410 Content Deleted</option>
650 <option value="451"<?php selected($rd_type, '451'); ?>>451 Content Unavailable</option>
651 </select></div>
652 <div class="field"><div class="lbl"><span>Destination URL</span></div>
653 <input class="ctrl" type="text" name="post_redirect_meta[url]" value="<?php echo esc_attr($rd_url); ?>" placeholder="https://example.com/new-url/"></div>
654 </div>
655 </div>
656 <?php endif; ?>
657
658 <?php if ($has_social): ?>
659 <div class="<?php echo esc_attr($pcls()); ?>" data-p="social">
660 <h2 class="p-title">&#128226; Social &amp; Open Graph</h2>
661 <p class="p-sub">How this post looks when shared on social platforms.</p>
662 <div class="switch-card">
663 <label class="switch"><input type="checkbox" name="_metasync_og_enabled" value="1"<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- checked_attr() returns the validated checked attribute.
664 echo esc_attr(self::checked_attr($og_en)); ?>></label>
665 <div class="m"><b>Enable Open Graph &amp; Social Media Tags</b><span>Adds Open Graph + Twitter Card tags for better sharing</span></div>
666 </div>
667
668 <div class="subhead"><span class="sq"></span> Open Graph</div>
669 <div class="field"><div class="lbl"><span>Title (og:title)</span></div><input class="ctrl" name="_metasync_og_title" maxlength="60" value="<?php echo esc_attr($og_title); ?>" placeholder="<?php echo esc_attr($og_defaults['title']); ?>" oninput="mssSocial()"></div>
670 <div class="field"><div class="lbl"><span>Description (og:description)</span></div><textarea class="ctrl" name="_metasync_og_description" maxlength="155" placeholder="<?php echo esc_attr($og_defaults['description']); ?>" oninput="mssSocial()"><?php echo esc_textarea($og_desc); ?></textarea></div>
671 <div class="field"><div class="lbl"><span>Image (og:image)</span></div>
672 <div class="mss-imgrow"><input class="ctrl ogimg" type="url" name="_metasync_og_image" value="<?php echo esc_attr($og_image); ?>" placeholder="https://example.com/image.jpg" oninput="mssSocial()"><button class="btn ghost sm mss-pick" data-target=".ogimg" type="button">Select</button></div>
673 <div class="hint">Recommended 1200&times;630px.</div></div>
674 <div class="row">
675 <div class="field"><div class="lbl"><span>Type (og:type)</span></div>
676 <select class="ctrl" name="_metasync_og_type">
677 <?php foreach (array('article', 'website', 'blog', 'product', 'video', 'music') as $t): ?>
678 <option value="<?php echo esc_attr($t); ?>"<?php selected($og_type, $t); ?>><?php echo esc_html(ucfirst($t)); ?></option>
679 <?php endforeach; ?>
680 </select></div>
681 <div class="field"><div class="lbl"><span>URL (og:url)</span></div><input class="ctrl" type="url" name="_metasync_og_url" value="<?php echo esc_attr($og_url); ?>" placeholder="<?php echo esc_attr($pretty); ?>"<?php echo $post->post_status === 'auto-draft' ? ' disabled' : ''; ?>></div>
682 </div>
683
684 <div class="subhead"><span class="sq"></span> Twitter Card</div>
685 <div class="field"><div class="lbl"><span>Card Type</span></div>
686 <select class="ctrl mssTw" name="_metasync_twitter_card" onchange="mssTwT(this)">
687 <option value="summary"<?php selected($tw_card, 'summary'); ?>>Summary</option>
688 <option value="summary_large_image"<?php selected($tw_card, 'summary_large_image'); ?>>Summary Large Image</option>
689 <option value="app"<?php selected($tw_card, 'app'); ?>>App</option>
690 <option value="player"<?php selected($tw_card, 'player'); ?>>Player</option>
691 </select></div>
692 <div class="row">
693 <div class="field"><div class="lbl"><span>Twitter Site</span></div><input class="ctrl" name="_metasync_twitter_site" value="<?php echo esc_attr($tw_site); ?>" placeholder="@yoursite"></div>
694 <div class="field"><div class="lbl"><span>Twitter Title</span></div><input class="ctrl" name="_metasync_twitter_title" maxlength="70" value="<?php echo esc_attr($tw_title); ?>" placeholder="<?php echo esc_attr($tw_title_placeholder); ?>" oninput="mssSocial()"></div>
695 </div>
696 <div class="field"><div class="lbl"><span>Twitter Description</span></div><textarea class="ctrl" name="_metasync_twitter_description" maxlength="200" placeholder="<?php echo esc_attr($tw_desc_placeholder); ?>" oninput="mssSocial()"><?php echo esc_textarea($tw_desc); ?></textarea></div>
697 <div class="row">
698 <div class="field"><div class="lbl"><span>Twitter Image</span></div>
699 <div class="mss-imgrow"><input class="ctrl twimg" type="url" name="_metasync_twitter_image" value="<?php echo esc_attr($tw_image); ?>" placeholder="Falls back to OG image" oninput="mssSocial()"><button class="btn ghost sm mss-pick" data-target=".twimg" type="button">Select</button></div></div>
700 <div class="field"><div class="lbl"><span>Image Alt</span></div><input class="ctrl" name="_metasync_twitter_image_alt" maxlength="420" value="<?php echo esc_attr($tw_alt); ?>" placeholder="Accessibility description"></div>
701 </div>
702
703 <div class="mssTwApp" style="display:<?php echo $tw_card === 'app' ? 'block' : 'none'; ?>">
704 <div class="subhead"><span class="sq"></span> App Card</div>
705 <div class="row">
706 <div class="field"><div class="lbl"><span>iPhone App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_iphone" value="<?php echo esc_attr($tw_app_iphone); ?>" placeholder="307234931"<?php echo esc_attr($tw_app_disabled); ?>></div>
707 <div class="field"><div class="lbl"><span>iPad App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_ipad" value="<?php echo esc_attr($tw_app_ipad); ?>" placeholder="307234931"<?php echo esc_attr($tw_app_disabled); ?>></div>
708 </div>
709 <div class="field"><div class="lbl"><span>Google Play App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_googleplay" value="<?php echo esc_attr($tw_app_gp); ?>" placeholder="com.android.app"<?php echo esc_attr($tw_app_disabled); ?>></div>
710 <div class="row">
711 <div class="field"><div class="lbl"><span>iPhone Custom URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_app_url_iphone" value="<?php echo esc_attr($tw_app_url_iphone); ?>" placeholder="myapp://"<?php echo esc_attr($tw_app_disabled); ?>></div>
712 <div class="field"><div class="lbl"><span>iPad Custom URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_app_url_ipad" value="<?php echo esc_attr($tw_app_url_ipad); ?>" placeholder="myapp://"<?php echo esc_attr($tw_app_disabled); ?>></div>
713 </div>
714 <div class="row">
715 <div class="field"><div class="lbl"><span>Google Play Custom URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_app_url_googleplay" value="<?php echo esc_attr($tw_app_url_gp); ?>" placeholder="myapp://"<?php echo esc_attr($tw_app_disabled); ?>></div>
716 <div class="field"><div class="lbl"><span>App Store Country</span></div>
717 <select class="ctrl" name="_metasync_twitter_app_country"<?php echo esc_attr($tw_app_disabled); ?>>
718 <option value=""<?php selected($tw_app_country, ''); ?>>US (Default)</option>
719 <?php foreach (array('GB' => 'United Kingdom', 'CA' => 'Canada', 'AU' => 'Australia', 'DE' => 'Germany', 'FR' => 'France', 'JP' => 'Japan', 'IN' => 'India', 'BR' => 'Brazil', 'MX' => 'Mexico') as $code => $country): ?>
720 <option value="<?php echo esc_attr($code); ?>"<?php selected($tw_app_country, $code); ?>><?php echo esc_html($country); ?></option>
721 <?php endforeach; ?>
722 </select>
723 <div class="hint">Required if your app is not available in the US App Store.</div>
724 </div>
725 </div>
726 </div>
727 <div class="mssTwPlayer" style="display:<?php echo $tw_card === 'player' ? 'block' : 'none'; ?>">
728 <div class="subhead"><span class="sq"></span> Player Card</div>
729 <div class="field"><div class="lbl"><span>Player URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_player" value="<?php echo esc_attr($tw_player); ?>" placeholder="https://example.com/player"<?php echo esc_attr($tw_player_disabled); ?>></div>
730 <div class="row">
731 <div class="field"><div class="lbl"><span>Width (px)</span></div><input class="ctrl short" type="number" name="_metasync_twitter_player_width" min="1" value="<?php echo esc_attr($tw_player_w); ?>" placeholder="1280"<?php echo esc_attr($tw_player_disabled); ?>></div>
732 <div class="field"><div class="lbl"><span>Height (px)</span></div><input class="ctrl short" type="number" name="_metasync_twitter_player_height" min="1" value="<?php echo esc_attr($tw_player_h); ?>" placeholder="720"<?php echo esc_attr($tw_player_disabled); ?>></div>
733 </div>
734 </div>
735
736 <div class="subhead"><span class="sq"></span> Social preview</div>
737 <div class="mss-sptabs">
738 <button type="button" class="on" onclick="mssSpTab(this,'facebook')">Facebook</button>
739 <button type="button" onclick="mssSpTab(this,'twitter')">Twitter / X</button>
740 <button type="button" onclick="mssSpTab(this,'linkedin')">LinkedIn</button>
741 </div>
742 <div class="spwrap">
743 <div class="spcard fb on" data-plat="facebook">
744 <div class="sp-head"><span class="sp-av"><?php echo esc_html($avatar); ?></span><div class="sp-meta"><b><?php echo esc_html($site); ?></b><span>2 hours ago &middot; &#127760;</span></div></div>
745 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
746 <div class="sp-body">
747 <div class="sp-url"><?php echo esc_html($host); ?></div>
748 <div class="sp-title spT"></div>
749 <div class="sp-desc spD"></div>
750 </div>
751 </div>
752 <div class="spcard tw" data-plat="twitter">
753 <div class="sp-head"><span class="sp-av"><?php echo esc_html($avatar); ?></span><div class="sp-meta"><b><?php echo esc_html($site); ?></b><span>@<?php echo esc_html($handle); ?> &middot; now</span></div></div>
754 <div class="tw-card">
755 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
756 <div class="sp-body">
757 <div class="sp-title spT"></div>
758 <div class="sp-desc spD"></div>
759 <div class="sp-url"><?php echo esc_html($host_lc); ?></div>
760 </div>
761 </div>
762 </div>
763 <div class="spcard li" data-plat="linkedin">
764 <div class="sp-head"><span class="sp-av"><?php echo esc_html($avatar); ?></span><div class="sp-meta"><b><?php echo esc_html($site); ?></b><span>Promoted</span></div></div>
765 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
766 <div class="sp-body">
767 <div class="sp-title spT"></div>
768 <div class="sp-url"><?php echo esc_html($host_lc); ?> &middot; 1 min read</div>
769 </div>
770 </div>
771 </div>
772 </div>
773 <?php endif; ?>
774
775 <?php if ($has_schema): ?>
776 <div class="<?php echo esc_attr($pcls()); ?>" data-p="schema">
777 <h2 class="p-title">&#128208; Schema Markup</h2>
778 <div class="mss-native">
779 <?php
780 if (!empty($this->schema_box['callback'])) {
781 call_user_func($this->schema_box['callback'], $post, $this->schema_box);
782 } else {
783 echo '<p class="p-sub">Schema builder unavailable on this screen.</p>';
784 }
785 ?>
786 </div>
787 </div>
788 <?php endif; ?>
789
790 <?php if ($has_video): ?>
791 <div class="<?php echo esc_attr($pcls()); ?>" data-p="video">
792 <h2 class="p-title">&#127916; Video Sitemap</h2>
793 <p class="p-sub">Override auto-detected video data. Leave blank to use auto-detection.</p>
794 <div class="field"><div class="lbl"><span>Video URL</span></div><input class="ctrl" type="url" name="metasync_video_url" value="<?php echo esc_attr($v_url); ?>" placeholder="https://www.youtube.com/watch?v=..."></div>
795 <div class="field"><div class="lbl"><span>Thumbnail URL</span></div><input class="ctrl" type="url" name="metasync_video_thumbnail" value="<?php echo esc_attr($v_thumb); ?>" placeholder="https://img.youtube.com/vi/.../hqdefault.jpg"></div>
796 <div class="field"><div class="lbl"><span>Video Title</span></div><input class="ctrl" name="metasync_video_title" value="<?php echo esc_attr($v_title); ?>" placeholder="Defaults to post title"></div>
797 <div class="field"><div class="lbl"><span>Video Description</span></div><textarea class="ctrl" name="metasync_video_description" placeholder="Defaults to post excerpt"><?php echo esc_textarea($v_desc); ?></textarea></div>
798 <div class="field"><div class="lbl"><span>Duration (seconds)</span></div><input class="ctrl short" type="number" name="metasync_video_duration" min="0" step="1" value="<?php echo esc_attr($v_dur); ?>" placeholder="300"></div>
799 </div>
800 <?php endif; ?>
801
802 </div>
803 </div>
804
805 <div class="mb-foot">
806 <span class="mss-foot-hint">Changes are saved when you update the post.</span>
807 </div>
808
809 <?php if ($managed): ?>
810 <div class="mss-lps-overlay">
811 <div class="mss-lps-card">
812 <div class="mss-lps-icon">&#128274;</div>
813 <h3>SEO managed by WebStudio</h3>
814 <p>This page's content is served as pre-built HTML before WordPress renders its <code>&lt;head&gt;</code>, so these SEO Suite settings won't apply. SEO for this page is managed by <b>WebStudio</b> (or OTTO, if enabled) to keep everything in sync.</p>
815 <div class="mss-lps-hint">Editing is disabled here to avoid conflicting values.</div>
816 </div>
817 </div>
818 <?php endif; ?>
819 </div>
820 </div>
821 </div>
822
823 <script>
824 (function(){
825 var root=document.getElementById('mss-root'); if(!root) return;
826 function byName(n){return root.querySelector('[name="'+n+'"]');}
827 function mssFld(n){var e=byName(n);return e?e.value.trim():'';}
828 window.mssKey=function(e,el){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); el.click(); } };
829 window.mssGo=function(el,p){
830 el.parentElement.querySelectorAll('.nav').forEach(function(n){n.classList.remove('active')});
831 el.classList.add('active');
832 root.querySelectorAll('.panel').forEach(function(pn){pn.classList.remove('active')});
833 var t=root.querySelector('.panel[data-p="'+p+'"]'); if(t)t.classList.add('active');
834 };
835 window.mssRc=function(){
836 var ti=root.querySelector('.seoT'), de=root.querySelector('.seoD'); if(!ti) return;
837 root.querySelector('.tC').textContent=ti.value.length;
838 root.querySelector('.dC').textContent=de.value.length;
839 root.querySelector('.gT').textContent=ti.value||ti.getAttribute('placeholder')||'Untitled';
840 root.querySelector('.gD').textContent=de.value||de.getAttribute('placeholder')||'No description set.';
841 };
842 window.mssTwT=function(sel){
843 var isApp=sel.value==='app', isPlayer=sel.value==='player';
844 var appSection=root.querySelector('.mssTwApp'), playerSection=root.querySelector('.mssTwPlayer');
845 if(appSection){
846 appSection.style.display=isApp?'block':'none';
847 appSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isApp;});
848 }
849 if(playerSection){
850 playerSection.style.display=isPlayer?'block':'none';
851 playerSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isPlayer;});
852 }
853 };
854 window.mssTheme=function(btn){
855 var dark=root.getAttribute('data-theme')==='dark';
856 root.setAttribute('data-theme',dark?'light':'dark');
857 btn.querySelector('.ti').textContent=dark?'\u{1F319}':'☀️';
858 btn.querySelector('.tl').textContent=dark?'Dark':'Light';
859 };
860 window.mssSpTab=function(btn,p){
861 btn.parentElement.querySelectorAll('button').forEach(function(b){b.classList.remove('on')});
862 btn.classList.add('on');
863 root.querySelectorAll('.spcard').forEach(function(c){c.classList.toggle('on', c.getAttribute('data-plat')===p)});
864 };
865 window.mssSocial=function(){
866 var seoT=root.querySelector('.seoT'), seoD=root.querySelector('.seoD');
867 var ogTitleEl=byName('_metasync_og_title');
868 var baseTitle=mssFld('_metasync_og_title') || (ogTitleEl&&ogTitleEl.getAttribute('placeholder')) || (seoT&&seoT.value.trim()) || 'Untitled';
869 var ogDescEl=byName('_metasync_og_description');
870 var baseDesc =mssFld('_metasync_og_description') || (ogDescEl&&ogDescEl.getAttribute('placeholder')) || (seoD&&seoD.value.trim()) || 'No description set.';
871 var baseImg =mssFld('_metasync_og_image');
872 var twTitle=mssFld('_metasync_twitter_title');
873 var twDesc =mssFld('_metasync_twitter_description');
874 var twImg =mssFld('_metasync_twitter_image');
875 root.querySelectorAll('.spcard').forEach(function(c){
876 var isTw=c.getAttribute('data-plat')==='twitter';
877 var T=isTw?(twTitle||baseTitle):baseTitle;
878 var D=isTw?(twDesc||baseDesc):baseDesc;
879 var I=isTw?(twImg||baseImg):baseImg;
880 var et=c.querySelector('.spT'); if(et)et.textContent=T;
881 var ed=c.querySelector('.spD'); if(ed)ed.textContent=D;
882 var eb=c.querySelector('.sp-img'); if(eb){ if(I){eb.style.backgroundImage='url("'+I.replace(/"/g,'')+'")';eb.classList.add('has');}else{eb.style.backgroundImage='';eb.classList.remove('has');} }
883 });
884 };
885 root.querySelectorAll('.mss-pick').forEach(function(btn){
886 btn.addEventListener('click',function(e){e.preventDefault();if(typeof wp==='undefined'||!wp.media)return;
887 var target=root.querySelector(btn.getAttribute('data-target'));
888 var frame=wp.media({title:'Select image',button:{text:'Use image'},multiple:false});
889 frame.on('select',function(){var img=frame.state().get('selection').first().toJSON();if(target){target.value=img.url;mssSocial();}});
890 frame.open();});
891 });
892 ['_metasync_og_title','_metasync_og_description','_metasync_og_image','_metasync_twitter_title','_metasync_twitter_description','_metasync_twitter_image'].forEach(function(n){
893 var e=byName(n); if(e) e.addEventListener('input',mssSocial);
894 });
895 // Language Alternates row management. Rows post as
896 // metasync_hreflang_rows[N][lang|region|url]; the save handler
897 // re-indexes and JSON-encodes them, so gaps in N are harmless — but
898 // a NEW row must never reuse an index a surviving row still owns:
899 // PHP's last-wins parsing would overwrite that row's values.
900 window.mssHreflangAdd=function(btn){
901 var wrap=root.querySelector('.mss-hreflang-rows');
902 if(!wrap) return;
903 var empty=root.querySelector('.mss-hreflang-empty'); if(empty) empty.style.display='none';
904 var idx=-1;
905 wrap.querySelectorAll('.mss-hrow input[name^="metasync_hreflang_rows["]').forEach(function(input){
906 var m=input.name.match(/^metasync_hreflang_rows\[(\d+)\]/);
907 if(m){ idx=Math.max(idx,parseInt(m[1],10)); }
908 });
909 idx=idx+1;
910 var row=document.createElement('div'); row.className='mss-hrow';
911 row.innerHTML='<div class="row">'
912 +'<div class="field"><div class="lbl"><span>Language</span></div>'
913 +'<input class="ctrl" name="metasync_hreflang_rows['+idx+'][lang]" value="" placeholder="en"></div>'
914 +'<div class="field"><div class="lbl"><span>Region</span></div>'
915 +'<input class="ctrl" name="metasync_hreflang_rows['+idx+'][region]" value="" placeholder="US"></div>'
916 +'</div>'
917 +'<div class="field"><div class="lbl"><span>URL</span></div>'
918 +'<div class="mss-hrow-url">'
919 +'<input class="ctrl" type="text" name="metasync_hreflang_rows['+idx+'][url]" value="" placeholder="https://example.com/en-us/">'
920 +'<button type="button" class="btn ghost sm mss-hreflang-remove" onclick="mssHreflangRemove(this)">Remove</button>'
921 +'</div></div>';
922 wrap.appendChild(row);
923 var lang=row.querySelector('input'); if(lang) lang.focus();
924 };
925 window.mssHreflangRemove=function(btn){
926 var row=btn.closest('.mss-hrow');
927 if(!row) return;
928 var wrap=row.parentNode;
929 row.parentNode.removeChild(row);
930 if(wrap && !wrap.querySelector('.mss-hrow')){
931 var empty=root.querySelector('.mss-hreflang-empty');
932 if(empty) empty.style.display='';
933 }
934 };
935 var twitterCard=byName('_metasync_twitter_card'); if(twitterCard)mssTwT(twitterCard); mssRc(); mssSocial();
936 })();
937 </script>
938 <?php
939 }
940
941 private function styles()
942 {
943 static $done = false;
944 if ($done) {
945 return;
946 }
947 $done = true;
948 ?>
949 <style>
950 #mss-root[data-theme="light"]{--card:#fff;--card-2:#f1f3f5;--rail:#fbfbfd;--text:#1a1f26;--muted:#6b7280;--faint:#9ca3af;--border:#e5e7eb;--border-soft:#eef0f3;--accent:#667eea;--purple:#764ba2;--grad:linear-gradient(135deg,#667eea 0%,#764ba2 100%);--grad-hover:linear-gradient(135deg,#5568d3 0%,#653f8c 100%);--grad-soft:linear-gradient(135deg,rgba(102,126,234,.10) 0%,rgba(118,75,162,.06) 100%);--success:#10b981;--warning:#f59e0b;--error:#ef4444;--field-bg:#fff;--shadow:0 4px 24px rgba(17,24,39,.08);--shadow-sm:0 1px 3px rgba(17,24,39,.06)}
951 #mss-root[data-theme="dark"]{--card:#1a1f26;--card-2:#222831;--rail:#171c23;--text:#fff;--muted:#9ca3af;--faint:#8b93a3;--border:#374151;--border-soft:#283242;--accent:#8ea2f0;--purple:#764ba2;--grad:linear-gradient(135deg,#667eea 0%,#764ba2 100%);--grad-hover:linear-gradient(135deg,#5568d3 0%,#653f8c 100%);--grad-soft:linear-gradient(135deg,rgba(102,126,234,.16) 0%,rgba(118,75,162,.10) 100%);--success:#10b981;--warning:#f59e0b;--error:#ef4444;--field-bg:#11161d;--shadow:0 8px 30px rgba(0,0,0,.45);--shadow-sm:0 1px 3px rgba(0,0,0,.4)}
952 #mss-root,#mss-root *{box-sizing:border-box}
953 #mss-root{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Inter,Oxygen,Ubuntu,sans-serif;color:var(--text);font-size:13.5px;line-height:1.55;margin:-6px -12px -12px;background:var(--card);overflow:hidden}
954 #mss-root .mb-head{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-soft)}
955 #mss-root .mb-head .badge{width:30px;height:30px;border-radius:9px;background:var(--grad);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:15px}
956 #mss-root .mb-head .t b{font-size:14.5px;color:var(--text)}
957 #mss-root .mb-head .t span{display:block;color:var(--muted);font-size:11.5px}
958 #mss-root .mss-actions{margin-left:auto;display:flex;gap:8px}
959 #mss-root .theme-toggle{display:flex;align-items:center;gap:7px;background:var(--grad);border:0;color:#fff;border-radius:20px;padding:6px 13px;cursor:pointer;font-size:12px;font-weight:600}
960 #mss-root .mb-body{display:grid;grid-template-columns:212px 1fr;min-height:430px}
961 @media (max-width:900px){#mss-root .mb-body{grid-template-columns:1fr}}
962 #mss-root .rail{background:var(--rail);border-right:1px solid var(--border-soft);padding:12px 10px;display:flex;flex-direction:column;gap:3px}
963 @media (max-width:900px){#mss-root .rail{flex-direction:row;overflow-x:auto;border-right:0;border-bottom:1px solid var(--border-soft)}}
964 #mss-root .nav{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:10px;cursor:pointer;color:var(--muted);font-weight:600;font-size:13px;border:1px solid transparent;white-space:nowrap;transition:.15s;position:relative}
965 #mss-root .nav:hover{background:var(--card-2);color:var(--text)}
966 #mss-root .nav.active{background:var(--grad-soft);color:var(--accent);border-color:rgba(102,126,234,.35)}
967 #mss-root .nav.active::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;background:var(--grad)}
968 #mss-root .nav .ic{width:20px;height:20px;flex:none;display:flex;align-items:center;justify-content:center;font-size:14px}
969 #mss-root .panel-wrap{padding:24px 26px;position:relative}
970 #mss-root .panel{display:none;animation:mssfade .22s ease}
971 #mss-root .panel.active{display:block}
972 @keyframes mssfade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
973 #mss-root .p-title{font-size:16px;font-weight:700;margin:0 0 3px;display:flex;align-items:center;gap:9px;color:var(--text)}
974 #mss-root .p-sub{color:var(--muted);font-size:12.5px;margin:0 0 20px}
975 #mss-root .sec{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--faint);margin:24px 0 13px;display:flex;align-items:center;gap:8px}
976 #mss-root .sec::after{content:"";flex:1;height:1px;background:var(--border-soft)}
977 #mss-root .sec-note{text-transform:none;letter-spacing:0;font-weight:500;color:var(--faint)}
978 #mss-root .subhead{font-size:14px;font-weight:700;color:var(--text);margin:24px 0 14px;display:flex;align-items:center;gap:9px;padding-bottom:9px;border-bottom:2px solid var(--border-soft)}
979 #mss-root .subhead .sq{width:9px;height:16px;border-radius:3px;background:var(--grad);flex:none}
980 #mss-root .field{margin:0 0 17px}
981 #mss-root .field:last-child{margin-bottom:0}
982 #mss-root .field .lbl{display:flex;justify-content:space-between;align-items:baseline;font-weight:600;margin-bottom:6px;font-size:12.5px;color:var(--text)}
983 #mss-root .field .lbl .cc{color:var(--faint);font-weight:600;font-size:11px}
984 #mss-root .ctrl{width:100%;padding:10px 12px;border:1.5px solid var(--border);border-radius:9px;font-size:13px;background:var(--field-bg);color:var(--text);transition:.15s;font-family:inherit;line-height:1.4;min-height:0;box-shadow:none}
985 #mss-root .ctrl:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(102,126,234,.16)}
986 #mss-root textarea.ctrl{min-height:78px;resize:vertical}
987 #mss-root input.short{max-width:140px}
988 #mss-root .mss-imgrow{display:flex;gap:9px}
989 #mss-root .hint{color:var(--faint);font-size:11.5px;margin-top:5px;line-height:1.5}
990 #mss-root .row{display:grid;grid-template-columns:1fr 1fr;gap:15px}
991 @media (max-width:560px){#mss-root .row{grid-template-columns:1fr}}
992 #mss-root .switch-card{display:flex;align-items:center;gap:13px;padding:13px 15px;background:var(--card-2);border:1px solid var(--border-soft);border-radius:11px;margin-bottom:18px}
993 #mss-root .switch{width:42px;height:24px;border-radius:24px;background:var(--border);position:relative;flex:none;cursor:pointer;transition:.18s;display:inline-block}
994 #mss-root .switch input{position:absolute;opacity:0;width:0;height:0}
995 #mss-root .switch::after{content:"";position:absolute;width:18px;height:18px;border-radius:50%;background:#fff;top:3px;left:3px;transition:.18s;box-shadow:0 1px 2px rgba(0,0,0,.3)}
996 #mss-root .switch:has(input:checked){background:var(--grad)}
997 #mss-root .switch:has(input:checked)::after{left:21px}
998 #mss-root .switch-card .m b{display:block;font-size:13px;color:var(--text)}
999 #mss-root .switch-card .m span{color:var(--muted);font-size:11.5px}
1000 #mss-root .chips{display:flex;flex-wrap:wrap;gap:9px}
1001 #mss-root .chip{display:flex;align-items:center;gap:8px;padding:9px 13px;border:1.5px solid var(--border);border-radius:10px;cursor:pointer;font-size:12.5px;font-weight:600;background:var(--field-bg);transition:.15s;user-select:none;color:var(--text)}
1002 #mss-root .chip input{position:absolute;opacity:0;width:0;height:0}
1003 #mss-root .chip:hover{border-color:var(--accent)}
1004 #mss-root .chip:has(input:checked){background:var(--grad-soft);border-color:var(--accent);color:var(--accent)}
1005 #mss-root .chip .box{width:15px;height:15px;border-radius:4px;border:1.5px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:10px;color:transparent;flex:none}
1006 #mss-root .chip:has(input:checked) .box{background:var(--accent);border-color:var(--accent);color:#fff}
1007 #mss-root .chip small{display:block;font-weight:500;color:var(--muted);font-size:10.5px;margin-top:2px;max-width:160px;line-height:1.35}
1008 #mss-root .chip:has(input:checked) small{color:var(--text)}
1009 #mss-root .mss-advchip{display:inline-flex;margin-bottom:7px}
1010 #mss-root .gprev{border:1px solid var(--border);border-radius:11px;padding:15px 17px;background:var(--field-bg)}
1011 #mss-root .gprev .u{color:var(--muted);font-size:12px}
1012 #mss-root .gprev .ti{color:#1a0dab;font-size:18px;margin:3px 0;font-weight:400}
1013 #mss-root[data-theme="dark"] .gprev .ti{color:#8ab4f8}
1014 #mss-root .gprev .de{color:var(--muted);font-size:12.5px}
1015 #mss-root .btn{background:var(--grad);color:#fff;border:0;padding:9px 17px;border-radius:9px;cursor:pointer;font-size:13px;font-weight:600;box-shadow:var(--shadow-sm);transition:.15s;height:auto;line-height:1.3}
1016 #mss-root .btn:hover{background:var(--grad-hover);color:#fff}
1017 #mss-root .btn.ghost{background:transparent;color:var(--accent);border:1.5px solid rgba(102,126,234,.45);box-shadow:none}
1018 #mss-root .btn.ghost:hover{background:var(--grad-soft)}
1019 #mss-root .btn.sm{padding:6px 12px;font-size:12px}
1020 #mss-root .mss-sptabs{display:flex;gap:2px;margin-bottom:13px;border-bottom:1px solid var(--border-soft)}
1021 #mss-root .mss-sptabs button{background:none;border:0;padding:8px 13px;font-size:12.5px;font-weight:600;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
1022 #mss-root .mss-sptabs button.on{color:var(--accent);border-bottom-color:var(--accent)}
1023 #mss-root .spcard{display:none;border:1px solid var(--border);border-radius:11px;overflow:hidden;background:var(--field-bg);max-width:524px}
1024 #mss-root .spcard.on{display:block}
1025 #mss-root .sp-head{display:flex;align-items:center;gap:10px;padding:11px 13px}
1026 #mss-root .sp-av{width:34px;height:34px;border-radius:50%;background:var(--grad);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:14px;flex:none}
1027 #mss-root .sp-meta b{font-size:13px;color:var(--text);display:block;line-height:1.3}
1028 #mss-root .sp-meta span{font-size:11px;color:var(--faint)}
1029 #mss-root .sp-img{height:210px;background:var(--card-2) center/cover no-repeat;display:flex;align-items:center;justify-content:center}
1030 #mss-root .sp-img.has .sp-noimg{display:none}
1031 #mss-root .sp-noimg{color:var(--faint);font-size:12px}
1032 #mss-root .sp-body{padding:11px 14px;border-top:1px solid var(--border-soft);background:var(--card-2)}
1033 #mss-root[data-theme="dark"] .sp-body{background:var(--card)}
1034 #mss-root .sp-url{font-size:10.5px;text-transform:uppercase;color:var(--faint);letter-spacing:.03em}
1035 #mss-root .sp-title{font-size:15px;font-weight:700;color:var(--text);margin:3px 0}
1036 #mss-root .sp-desc{font-size:12.5px;color:var(--muted)}
1037 #mss-root .spcard.tw .tw-card{margin:0 13px 14px;border:1px solid var(--border);border-radius:15px;overflow:hidden}
1038 #mss-root .spcard.tw .sp-body{border-top:0;background:var(--field-bg);display:flex;flex-direction:column}
1039 #mss-root .spcard.tw .sp-title{font-size:14px;margin:0 0 3px;order:1}
1040 #mss-root .spcard.tw .sp-desc{order:2}
1041 #mss-root .spcard.tw .sp-url{order:0;text-transform:none;margin-bottom:4px}
1042 #mss-root .spcard.li .sp-title{font-size:14.5px}
1043 #mss-root .mb-foot{display:flex;align-items:center;gap:14px;padding:13px 26px;border-top:1px solid var(--border-soft);background:var(--rail)}
1044 #mss-root .mss-foot-hint{font-size:12px;color:var(--muted)}
1045 #mss-root .mss-lockable{position:relative}
1046 #mss-root .mss-lps-overlay{position:absolute;inset:0;display:none;align-items:center;justify-content:center;padding:26px;z-index:6;background:rgba(248,249,250,.62);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
1047 #mss-root[data-theme="dark"] .mss-lps-overlay{background:rgba(15,20,25,.66)}
1048 #mss-root.lps .mss-lps-overlay{display:flex}
1049 #mss-root .mss-lps-card{max-width:440px;text-align:center;background:var(--card);border:1px solid var(--border);border-radius:18px;box-shadow:var(--shadow);padding:32px 30px}
1050 #mss-root .mss-lps-icon{width:58px;height:58px;margin:0 auto 16px;border-radius:16px;background:var(--grad);display:flex;align-items:center;justify-content:center;font-size:27px;box-shadow:var(--shadow-sm)}
1051 #mss-root .mss-lps-card h3{margin:0 0 9px;font-size:18px;color:var(--text)}
1052 #mss-root .mss-lps-card p{margin:0 0 20px;color:var(--muted);font-size:13px;line-height:1.65}
1053 #mss-root .mss-lps-card p b{color:var(--text)}
1054 #mss-root .mss-lps-card p code{background:var(--card-2);padding:1px 5px;border-radius:4px;font-size:11px}
1055 #mss-root .mss-lps-hint{margin-top:15px;font-size:11.5px;color:var(--faint)}
1056 #mss-root .mss-ro{width:100%;padding:10px 12px;border:1.5px dashed var(--border);border-radius:9px;background:var(--card-2);color:var(--text);font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px}
1057 #mss-root .ro-badge{font-weight:600;font-size:10.5px;color:var(--accent);background:var(--grad-soft);border:1px solid rgba(102,126,234,.30);border-radius:20px;padding:2px 9px}
1058 #mss-root .mss-native input[type=text],#mss-root .mss-native input[type=url],#mss-root .mss-native input[type=number],#mss-root .mss-native textarea,#mss-root .mss-native select{border-radius:8px !important}
1059 /* The builder's own `.schema-field input {width:100%}` also hits checkboxes and radios,
1060 stretching them into a full-width block. Core styles these with
1061 -webkit-appearance:none, so they have NO intrinsic size and depend entirely on an
1062 explicit width/height — restate core's 1rem box rather than using width:auto, which
1063 would collapse the control to nothing while unchecked (the checked state only stays
1064 visible because core paints a ::before glyph over it). */
1065 #mss-root .mss-native input[type=checkbox],
1066 #mss-root .mss-native input[type=radio]{width:1rem !important;min-width:1rem !important;max-width:1rem !important;height:1rem !important;flex:0 0 auto;padding:0 !important;margin:0 7px 0 0;vertical-align:middle}
1067 #mss-root .mss-native input[type=radio]{border-radius:50% !important}
1068 #mss-root .mss-native .schema-field label:has(input[type=checkbox]),
1069 #mss-root .mss-native .schema-field label:has(input[type=radio]){display:flex;align-items:center;gap:2px}
1070 /* Dark mode: theme the embedded native Schema builder. Every surface the builder paints
1071 light — whether from its stylesheet or an inline style — is listed here. Text is only
1072 forced light on surfaces this block also darkens, so nothing ends up light-on-light. */
1073 #mss-root[data-theme="dark"] .mss-native{color:var(--text)}
1074 #mss-root[data-theme="dark"] .mss-native h1,#mss-root[data-theme="dark"] .mss-native h2,#mss-root[data-theme="dark"] .mss-native h3,#mss-root[data-theme="dark"] .mss-native h4,#mss-root[data-theme="dark"] .mss-native h5,#mss-root[data-theme="dark"] .mss-native label,#mss-root[data-theme="dark"] .mss-native strong,#mss-root[data-theme="dark"] .mss-native p{color:var(--text) !important}
1075 #mss-root[data-theme="dark"] .mss-native .description,#mss-root[data-theme="dark"] .mss-native small{color:var(--muted) !important}
1076 /* Light surfaces → dark. Class/ID list mirrors schema-markup-admin.css; the [style*=]
1077 matchers catch the builder's inline backgrounds (#f5f5f5, #f9f9f9, #fff, white). */
1078 #mss-root[data-theme="dark"] .mss-native .schema-types-container,
1079 #mss-root[data-theme="dark"] .mss-native .schema-types-header,
1080 #mss-root[data-theme="dark"] .mss-native .schema-type-item,
1081 #mss-root[data-theme="dark"] .mss-native .schema-type-header,
1082 #mss-root[data-theme="dark"] .mss-native .schema-type-content,
1083 #mss-root[data-theme="dark"] .mss-native .schema-fields-container,
1084 #mss-root[data-theme="dark"] .mss-native .schema-field,
1085 #mss-root[data-theme="dark"] .mss-native .schema-field-group,
1086 #mss-root[data-theme="dark"] .mss-native .schema-override-header,
1087 #mss-root[data-theme="dark"] .mss-native .schema-override-content,
1088 #mss-root[data-theme="dark"] .mss-native .no-schema-types,
1089 #mss-root[data-theme="dark"] .mss-native .faq-item,
1090 #mss-root[data-theme="dark"] .mss-native .instruction-item,
1091 #mss-root[data-theme="dark"] .mss-native .logo-upload-container,
1092 #mss-root[data-theme="dark"] .mss-native .logo-preview,
1093 #mss-root[data-theme="dark"] .mss-native #schema-preview-output,
1094 #mss-root[data-theme="dark"] .mss-native #schema-type-modal > div,
1095 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff"],
1096 #mss-root[data-theme="dark"] .mss-native [style*="background:#fff"],
1097 #mss-root[data-theme="dark"] .mss-native [style*="background: white"],
1098 #mss-root[data-theme="dark"] .mss-native [style*="background:white"],
1099 #mss-root[data-theme="dark"] .mss-native [style*="background: #f9f9f9"],
1100 #mss-root[data-theme="dark"] .mss-native [style*="background:#f9f9f9"],
1101 #mss-root[data-theme="dark"] .mss-native [style*="background: #f5f5f5"],
1102 #mss-root[data-theme="dark"] .mss-native [style*="background:#f5f5f5"],
1103 #mss-root[data-theme="dark"] .mss-native [style*="#ffffff"]{background-color:var(--card-2) !important;border-color:var(--border) !important}
1104 /* Notices keep their semantic colour instead of being flattened to a grey surface. */
1105 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
1106 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff3cd"],
1107 #mss-root[data-theme="dark"] .mss-native [style*="background:#fff3cd"]{background-color:rgba(245,158,11,.14) !important;border-color:rgba(245,158,11,.45) !important}
1108 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
1109 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning *,
1110 #mss-root[data-theme="dark"] .mss-native [style*="#fff3cd"] *,
1111 #mss-root[data-theme="dark"] .mss-native [style*="color: #856404"],
1112 #mss-root[data-theme="dark"] .mss-native [style*="color:#856404"]{color:#fcd34d !important}
1113 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
1114 #mss-root[data-theme="dark"] .mss-native [style*="background: #e7f3ff"],
1115 #mss-root[data-theme="dark"] .mss-native [style*="background:#e7f3ff"]{background-color:rgba(102,126,234,.14) !important;border-color:rgba(102,126,234,.45) !important}
1116 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
1117 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info *{color:#c7d2fe !important}
1118 /* Dark inline text on the surfaces darkened above → readable. */
1119 #mss-root[data-theme="dark"] .mss-native [style*="color: #333"],
1120 #mss-root[data-theme="dark"] .mss-native [style*="color:#333"],
1121 #mss-root[data-theme="dark"] .mss-native [style*="color: #000"],
1122 #mss-root[data-theme="dark"] .mss-native [style*="color:#000"],
1123 #mss-root[data-theme="dark"] .mss-native [style*="color: #666"],
1124 #mss-root[data-theme="dark"] .mss-native [style*="color:#666"]{color:var(--text) !important}
1125 #mss-root[data-theme="dark"] .mss-native input[type=text],#mss-root[data-theme="dark"] .mss-native input[type=url],#mss-root[data-theme="dark"] .mss-native input[type=number],#mss-root[data-theme="dark"] .mss-native input[type=email],#mss-root[data-theme="dark"] .mss-native textarea,#mss-root[data-theme="dark"] .mss-native select{background:var(--field-bg) !important;color:var(--text) !important;border-color:var(--border) !important}
1126 /* Checkboxes/radios: core paints them white-on-dark-border, which glares against this
1127 box. Unchecked gets the field surface with a visible border; checked gets the brand
1128 accent, which keeps enough contrast for core's white ::before tick. */
1129 #mss-root[data-theme="dark"] .mss-native input[type=checkbox],
1130 #mss-root[data-theme="dark"] .mss-native input[type=radio]{background:var(--field-bg) !important;border-color:#6b7280 !important}
1131 #mss-root[data-theme="dark"] .mss-native input[type=checkbox]:checked,
1132 #mss-root[data-theme="dark"] .mss-native input[type=radio]:checked{background:var(--accent) !important;border-color:var(--accent) !important}
1133 /* Core's .button is painted light by wp-admin; keep it legible on the dark surface.
1134 Destructive buttons (the builder paints them red) are left alone. */
1135 #mss-root[data-theme="dark"] .mss-native .button,
1136 #mss-root[data-theme="dark"] .mss-native .button-secondary{background:var(--card-2) !important;color:var(--accent) !important;border-color:rgba(102,126,234,.45) !important;text-shadow:none !important}
1137 #mss-root[data-theme="dark"] .mss-native .button:hover,
1138 #mss-root[data-theme="dark"] .mss-native .button-secondary:hover{background:var(--grad-soft) !important;color:var(--text) !important}
1139 #mss-root[data-theme="dark"] .mss-native .button-primary{background:var(--grad) !important;color:#fff !important;border-color:transparent !important}
1140 /* .remove-schema-type is a text-style .button (red text, no fill) — keep it red rather
1141 than letting the .button rule above recolour it. .remove-faq-item / .remove-item are
1142 solid-red fills with white text and are deliberately left untouched. */
1143 #mss-root[data-theme="dark"] .mss-native .remove-schema-type,
1144 #mss-root[data-theme="dark"] .mss-native .remove-schema-type .dashicons{color:#f87171 !important;border-color:rgba(248,113,113,.45) !important}
1145 /* Inline '?' tooltip on the Breadcrumb Title label. */
1146 #mss-root .mss-tip{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--card-2);border:1px solid var(--border);color:var(--muted);font-size:11px;font-weight:700;cursor:help;flex:none}
1147 /* Language Alternates rows. */
1148 #mss-root .mss-hreflang-rows{display:flex;flex-direction:column;gap:14px;margin-bottom:16px}
1149 #mss-root .mss-hrow{border:1px solid var(--border-soft);border-radius:10px;padding:13px 14px;background:var(--card-2)}
1150 #mss-root .mss-hrow .row{grid-template-columns:1fr 1fr;gap:13px;margin-bottom:0}
1151 #mss-root .mss-hrow .field{margin-bottom:11px}
1152 #mss-root .mss-hrow .field:last-child{margin-bottom:0}
1153 #mss-root .mss-hrow-url{display:flex;gap:9px;align-items:flex-start}
1154 #mss-root .mss-hrow-url .ctrl{flex:1}
1155 #mss-root .mss-hreflang-empty{color:var(--muted);font-size:12.5px;padding:14px;border:1.5px dashed var(--border);border-radius:10px;text-align:center;margin-bottom:16px;background:var(--card-2)}
1156 #mss-root .mss-hreflang-empty p{margin:0}
1157 </style>
1158 <?php
1159 }
1160 }
1161