PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.22
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.22
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-seo-suite.php

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

937 lines 64.0 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'])) {
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 return is_array($value) ? '' : (string) $value;
164 }
165
166 private static function checked_attr($cond)
167 {
168 return $cond ? ' checked' : '';
169 }
170
171 /** A clean, "pretty" permalink for placeholders/preview (avoids ?p=ID on drafts). */
172 private static function pretty_permalink($post)
173 {
174 $link = get_permalink($post->ID);
175 if ($link
176 && strpos($link, '?p=') === false
177 && strpos($link, '?page_id=') === false
178 && strpos($link, 'preview=') === false) {
179 return $link;
180 }
181 $slug = $post->post_name ? $post->post_name : sanitize_title(get_the_title($post->ID));
182 if (!$slug) {
183 $slug = 'your-post';
184 }
185 return user_trailingslashit(home_url('/' . $slug . '/'));
186 }
187
188 /**
189 * Render the unified box.
190 */
191 public function render($post)
192 {
193 $P = $this->present;
194 $id = $post->ID;
195 $a = 'esc_attr';
196
197 $managed = $this->is_managed_page($id);
198
199 $has_seo = isset($P['metasync-seo-meta']) || isset($P['metasync-seo-lps-notice']);
200 $has_crob = isset($P['common-robots-meta']);
201 $has_arob = isset($P['advance-robots-meta']);
202 $has_robots = $has_crob || $has_arob;
203 $has_canon = isset($P['post-canonical-meta']);
204 $has_redir = isset($P['post-redirection-meta']);
205 $has_social = isset($P['metasync_opengraph_meta_box']);
206 $has_schema = isset($P['metasync-schema-markup']);
207 $has_video = isset($P['metasync-video-sitemap-meta']);
208
209 // Current values.
210 $seo_title = self::v($id, '_metasync_seo_title');
211 $seo_desc = self::v($id, '_metasync_seo_desc');
212 $otto_title = self::v($id, '_metasync_otto_title');
213 $otto_desc = self::v($id, '_metasync_otto_description');
214 $otto_kw = self::v($id, '_metasync_otto_keywords');
215
216 // Robots: post meta first, falling back to the site-wide defaults exactly as
217 // the standalone Common/Advance Robots boxes do (new option spelling first,
218 // then the legacy "mata" spelling).
219 $cr = get_post_meta($id, 'metasync_common_robots', true);
220 if (!$cr && class_exists('Metasync')) {
221 $cr = Metasync::get_option('common_robots_meta') ?? Metasync::get_option('common_robots_mata') ?? '';
222 }
223 if (!is_array($cr)) {
224 $cr = array();
225 }
226 $ar = get_post_meta($id, 'metasync_advance_robots', true);
227 if (!$ar && class_exists('Metasync')) {
228 $ar = Metasync::get_option('advance_robots_meta') ?? Metasync::get_option('advance_robots_mata') ?? '';
229 }
230 if (!is_array($ar)) {
231 $ar = array();
232 }
233 $ar_snip_e = isset($ar['max-snippet']['enable']) ? $ar['max-snippet']['enable'] : '';
234 $ar_snip_l = isset($ar['max-snippet']['length']) ? $ar['max-snippet']['length'] : '';
235 $ar_vid_e = isset($ar['max-video-preview']['enable']) ? $ar['max-video-preview']['enable'] : '';
236 $ar_vid_l = isset($ar['max-video-preview']['length']) ? $ar['max-video-preview']['length'] : '';
237 $ar_img_e = isset($ar['max-image-preview']['enable']) ? $ar['max-image-preview']['enable'] : '';
238 $ar_img_l = isset($ar['max-image-preview']['length']) ? $ar['max-image-preview']['length'] : '';
239
240 // Canonical: same sanitise-and-repair path as the standalone Canonical box.
241 // A bare reset() here would re-corrupt nested-array rows to the string "Array".
242 $raw_canonical = get_post_meta($id, 'meta_canonical', true);
243 if (class_exists('Metasync_Canonical_Sanitizer')) {
244 $canon = Metasync_Canonical_Sanitizer::sanitize($raw_canonical);
245 if (is_array($raw_canonical) || Metasync_Canonical_Sanitizer::is_corrupted($raw_canonical)) {
246 if ($canon !== '') {
247 update_post_meta($id, 'meta_canonical', $canon);
248 } else {
249 delete_post_meta($id, 'meta_canonical');
250 }
251 } elseif ($canon === '' && is_string($raw_canonical) && trim($raw_canonical) !== '') {
252 // Stored value the validator doesn't recognise: show it as-is so a
253 // routine post save doesn't silently wipe it.
254 $canon = trim($raw_canonical);
255 }
256 } else {
257 $canon = is_array($raw_canonical) ? '' : (string) $raw_canonical;
258 }
259 $pretty = self::pretty_permalink($post);
260
261 $rd = get_post_meta($id, 'metasync_post_redirection_meta', true);
262 if (!is_array($rd)) {
263 $rd = array();
264 }
265 $rd_en = (isset($rd['enable']) ? $rd['enable'] : '') === 'true';
266 $rd_type = isset($rd['type']) ? $rd['type'] : '301';
267 $rd_url = isset($rd['url']) ? $rd['url'] : '';
268
269 // The standalone Open Graph box defaults this setting to enabled when
270 // no value has been saved yet. Preserve that behaviour: only an
271 // explicit '0' represents an editor opting out.
272 $og_en_value = get_post_meta($id, '_metasync_og_enabled', true);
273 $og_en = ($og_en_value !== '0');
274 // Match the standalone Open Graph box exactly: its computed defaults are
275 // real form values (and are therefore persisted on save), not placeholders.
276 $og_defaults = array('title' => '', 'description' => '', 'image' => '');
277 $opengraph = class_exists('Metasync_OpenGraph') ? Metasync_OpenGraph::get_instance() : null;
278 if ($opengraph) {
279 $og_defaults = $opengraph->get_default_og_values($id);
280 }
281 $og_title = self::v($id, '_metasync_og_title') ?: $og_defaults['title'];
282 $og_desc = self::v($id, '_metasync_og_description') ?: $og_defaults['description'];
283 $og_image = self::v($id, '_metasync_og_image') ?: $og_defaults['image'];
284 $og_type = self::v($id, '_metasync_og_type') ?: 'article';
285 $og_url = self::v($id, '_metasync_og_url');
286 if ($og_url === '') {
287 $og_url = $opengraph ? $opengraph->get_canonical_url($post) : $pretty;
288 }
289 $tw_card = self::v($id, '_metasync_twitter_card') ?: 'summary_large_image';
290 $tw_site = self::v($id, '_metasync_twitter_site');
291 $tw_title = self::v($id, '_metasync_twitter_title') ?: $og_title;
292 $tw_desc = self::v($id, '_metasync_twitter_description') ?: $og_desc;
293 $tw_image = self::v($id, '_metasync_twitter_image') ?: $og_image;
294 $tw_alt = self::v($id, '_metasync_twitter_image_alt');
295 $tw_app_iphone = self::v($id, '_metasync_twitter_app_id_iphone');
296 $tw_app_ipad = self::v($id, '_metasync_twitter_app_id_ipad');
297 $tw_app_gp = self::v($id, '_metasync_twitter_app_id_googleplay');
298 $tw_app_url_iphone = self::v($id, '_metasync_twitter_app_url_iphone');
299 $tw_app_url_ipad = self::v($id, '_metasync_twitter_app_url_ipad');
300 $tw_app_url_gp = self::v($id, '_metasync_twitter_app_url_googleplay');
301 $tw_app_country = self::v($id, '_metasync_twitter_app_country');
302 $tw_player = self::v($id, '_metasync_twitter_player');
303 $tw_player_w = self::v($id, '_metasync_twitter_player_width');
304 $tw_player_h = self::v($id, '_metasync_twitter_player_height');
305 $tw_app_disabled = $tw_card === 'app' ? '' : ' disabled';
306 $tw_player_disabled = $tw_card === 'player' ? '' : ' disabled';
307
308 $v_url = self::v($id, '_metasync_video_url');
309 $v_thumb = self::v($id, '_metasync_video_thumbnail');
310 $v_title = self::v($id, '_metasync_video_title');
311 $v_desc = self::v($id, '_metasync_video_description');
312 $v_dur = self::v($id, '_metasync_video_duration');
313
314 $host = strtoupper((string) wp_parse_url(home_url(), PHP_URL_HOST));
315 $host_lc = strtolower($host);
316 $prev_url_disp = preg_replace('#^https?://#', '', (string) $pretty);
317 $site = get_bloginfo('name');
318 if ($site === '') {
319 $site = $host;
320 }
321 $avatar = strtoupper(function_exists('mb_substr') ? mb_substr($site, 0, 1) : substr($site, 0, 1));
322 $handle = sanitize_title($site) ?: 'site';
323
324 $this->styles();
325 ?>
326 <div id="mss-root" data-theme="light"<?php echo $managed ? ' class="lps"' : ''; ?>>
327 <?php
328 // On managed pages (LPS / custom-HTML) we emit NO nonces, so nothing here can
329 // overwrite the externally-managed SEO for those pages.
330 if (!$managed) {
331 if ($has_seo) { wp_nonce_field('metasync_seo_meta_nonce', 'metasync_seo_meta_nonce'); }
332 if ($has_crob) { wp_nonce_field('metasync_common_robots_nonce', 'metasync_common_robots_nonce'); }
333 if ($has_arob) { wp_nonce_field('metasync_advance_robots_nonce', 'metasync_advance_robots_nonce'); }
334 if ($has_canon) { wp_nonce_field('metasync_post_canonical_nonce', 'metasync_post_canonical_nonce'); }
335 if ($has_redir) { wp_nonce_field('metasync_post_redirection_nonce', 'metasync_post_redirection_nonce'); }
336 if ($has_social) { wp_nonce_field('metasync_opengraph_nonce', 'metasync_opengraph_nonce'); }
337 if ($has_video) { wp_nonce_field('metasync_video_sitemap_meta_nonce', 'metasync_video_sitemap_meta_nonce'); }
338 }
339 ?>
340 <div class="metabox">
341 <div class="mb-head">
342 <div class="badge">M</div>
343 <div class="t"><b>SEO Suite</b><span>On-page SEO for this post</span></div>
344 <div class="mss-actions">
345 <button type="button" class="theme-toggle" onclick="mssTheme(this)"><span class="ti">&#127769;</span> <span class="tl">Dark</span></button>
346 </div>
347 </div>
348
349 <div class="mss-lockable">
350 <div class="mb-body"<?php echo $managed ? ' inert' : ''; ?>>
351 <div class="rail">
352 <?php
353 $first = true;
354 $tab = function ($key, $icon, $label) use (&$first) {
355 echo '<div class="nav' . ($first ? ' active' : '') . '" role="button" tabindex="0" onclick="mssGo(this,\'' . esc_attr($key) . '\')" onkeydown="mssKey(event,this)"><span class="ic">' . $icon . '</span> ' . $label . '</div>';
356 $first = false;
357 };
358 if ($has_seo) { $tab('seo', '&#128269;', 'SEO'); }
359 if ($has_robots) { $tab('robots', '&#129302;', 'Robots'); }
360 if ($has_canon) { $tab('canonical', '&#128279;', 'Canonical'); }
361 if ($has_redir) { $tab('redirect', '&#8618;&#65039;', 'Redirection'); }
362 if ($has_social) { $tab('social', '&#128226;', 'Social &amp; OG'); }
363 if ($has_schema) { $tab('schema', '&#128208;', 'Schema'); }
364 if ($has_video) { $tab('video', '&#127916;', 'Video'); }
365 ?>
366 </div>
367
368 <div class="panel-wrap">
369 <?php
370 $pfirst = true;
371 $pcls = function () use (&$pfirst) {
372 $c = 'panel' . ($pfirst ? ' active' : '');
373 $pfirst = false;
374 return $c;
375 };
376 ?>
377
378 <?php if ($has_seo): ?>
379 <div class="<?php echo $pcls(); ?>" data-p="seo">
380 <h2 class="p-title">&#128269; Search Appearance</h2>
381 <p class="p-sub">Set the SEO Title and Meta Description. Leave blank to use the OTTO suggestion.</p>
382 <div class="field">
383 <div class="lbl"><span>SEO Title</span><span class="cc"><span class="tC">0</span>/60</span></div>
384 <input class="ctrl seoT" name="metasync_seo_title" value="<?php echo $a($seo_title); ?>" placeholder="<?php echo $a($otto_title); ?>" oninput="mssRc()">
385 </div>
386 <div class="field">
387 <div class="lbl"><span>Meta Description</span><span class="cc"><span class="dC">0</span>/160</span></div>
388 <textarea class="ctrl seoD" name="metasync_seo_desc" placeholder="<?php echo $a($otto_desc); ?>" oninput="mssRc()"><?php echo esc_textarea($seo_desc); ?></textarea>
389 </div>
390 <?php if ($otto_kw !== ''): ?>
391 <div class="field">
392 <div class="lbl"><span>Focus Keyword</span><span class="ro-badge">Managed by OTTO</span></div>
393 <div class="mss-ro"><span class="lock">&#128274;</span> <?php echo esc_html($otto_kw); ?></div>
394 <div class="hint">Set in Search Atlas &mdash; shown here for reference. Read-only.</div>
395 </div>
396 <?php endif; ?>
397 <div class="sec">Preview</div>
398 <div class="gprev">
399 <div class="u"><?php echo esc_html($prev_url_disp); ?></div>
400 <div class="ti gT"><?php echo esc_html($seo_title ?: ($otto_title ?: get_the_title($id))); ?></div>
401 <div class="de gD"><?php echo esc_html($seo_desc ?: ($otto_desc ?: 'No description set.')); ?></div>
402 </div>
403 </div>
404 <?php endif; ?>
405
406 <?php if ($has_robots): ?>
407 <div class="<?php echo $pcls(); ?>" data-p="robots">
408 <h2 class="p-title">&#129302; Robots Meta</h2>
409 <p class="p-sub">Crawler directives for this post. These are independent flags &mdash; combine as needed.</p>
410 <?php if ($has_crob): ?>
411 <div class="sec">Common directives</div>
412 <div class="chips">
413 <?php
414 $common = array(
415 'index' => array('Index', 'Allow search engines to index this page'),
416 'noindex' => array('No Index', 'Keep this page out of results'),
417 'nofollow' => array('No Follow', "Don't follow links on this page"),
418 'noarchive' => array('No Archive', 'No cached copy'),
419 'noimageindex' => array('No Image Index', 'Exclude images from image search'),
420 'nosnippet' => array('No Snippet', 'No text snippet in results'),
421 );
422 foreach ($common as $key => $c): ?>
423 <label class="chip">
424 <input type="checkbox" name="common_robots_meta[<?php echo $a($key); ?>]" value="<?php echo $a($key); ?>"<?php echo isset($cr[$key]) ? self::checked_attr($cr[$key] === $key) : ''; ?>>
425 <span class="box">&#10003;</span><div><?php echo esc_html($c[0]); ?><small><?php echo esc_html($c[1]); ?></small></div>
426 </label>
427 <?php endforeach; ?>
428 </div>
429 <?php endif; ?>
430 <?php if ($has_arob): ?>
431 <div class="sec">Advanced limits <span class="sec-note">&mdash; tick a limit to apply it</span></div>
432 <div class="row">
433 <div class="field">
434 <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>
435 <input class="ctrl" type="number" name="advanced_robots_meta[max-snippet][length]" value="<?php echo $a($ar_snip_l); ?>" min="-1" placeholder="e.g. 160">
436 <div class="hint">Max snippet length in characters.</div></div>
437 <div class="field">
438 <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>
439 <input class="ctrl" type="number" name="advanced_robots_meta[max-video-preview][length]" value="<?php echo $a($ar_vid_l); ?>" min="-1" placeholder="e.g. -1">
440 <div class="hint">Max preview duration in seconds.</div></div>
441 </div>
442 <div class="field">
443 <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>
444 <select class="ctrl" name="advanced_robots_meta[max-image-preview][length]">
445 <option value="large"<?php selected($ar_img_l, 'large'); ?>>Large</option>
446 <option value="standard"<?php selected($ar_img_l, 'standard'); ?>>Standard</option>
447 <option value="none"<?php selected($ar_img_l, 'none'); ?>>None</option>
448 </select></div>
449 <?php endif; ?>
450 </div>
451 <?php endif; ?>
452
453 <?php if ($has_canon): ?>
454 <div class="<?php echo $pcls(); ?>" data-p="canonical">
455 <h2 class="p-title">&#128279; Canonical URL</h2>
456 <p class="p-sub">Point search engines to the preferred version of this content.</p>
457 <div class="field">
458 <div class="lbl"><span>Canonical URL</span></div>
459 <input class="ctrl" type="text" name="post_canonical_url_meta" value="<?php echo $a($canon); ?>" placeholder="<?php echo $a($pretty); ?>">
460 <div class="hint">Leave blank to use this post's own URL.</div>
461 </div>
462 </div>
463 <?php endif; ?>
464
465 <?php if ($has_redir): ?>
466 <div class="<?php echo $pcls(); ?>" data-p="redirect">
467 <h2 class="p-title">&#8618;&#65039; Redirection</h2>
468 <p class="p-sub">Send visitors of this post somewhere else.</p>
469 <div class="switch-card">
470 <label class="switch"><input type="checkbox" name="post_redirect_meta[enable]" value="true"<?php echo self::checked_attr($rd_en); ?>></label>
471 <div class="m"><b>Enable redirection</b><span>Send visitors of this post to another URL</span></div>
472 </div>
473 <div class="row">
474 <div class="field"><div class="lbl"><span>Type</span></div>
475 <select class="ctrl" name="post_redirect_meta[type]">
476 <option value="301"<?php selected($rd_type, '301'); ?>>301 Permanent Move</option>
477 <option value="302"<?php selected($rd_type, '302'); ?>>302 Temporary Move</option>
478 <option value="307"<?php selected($rd_type, '307'); ?>>307 Temporary Redirect</option>
479 <option value="410"<?php selected($rd_type, '410'); ?>>410 Content Deleted</option>
480 <option value="451"<?php selected($rd_type, '451'); ?>>451 Content Unavailable</option>
481 </select></div>
482 <div class="field"><div class="lbl"><span>Destination URL</span></div>
483 <input class="ctrl" type="text" name="post_redirect_meta[url]" value="<?php echo $a($rd_url); ?>" placeholder="https://example.com/new-url/"></div>
484 </div>
485 </div>
486 <?php endif; ?>
487
488 <?php if ($has_social): ?>
489 <div class="<?php echo $pcls(); ?>" data-p="social">
490 <h2 class="p-title">&#128226; Social &amp; Open Graph</h2>
491 <p class="p-sub">How this post looks when shared on social platforms.</p>
492 <div class="switch-card">
493 <label class="switch"><input type="checkbox" name="_metasync_og_enabled" value="1"<?php echo self::checked_attr($og_en); ?>></label>
494 <div class="m"><b>Enable Open Graph &amp; Social Media Tags</b><span>Adds Open Graph + Twitter Card tags for better sharing</span></div>
495 </div>
496
497 <div class="subhead"><span class="sq"></span> Open Graph</div>
498 <div class="field"><div class="lbl"><span>Title (og:title)</span></div><input class="ctrl" name="_metasync_og_title" maxlength="60" value="<?php echo $a($og_title); ?>" placeholder="<?php echo $a(get_the_title($id)); ?>" oninput="mssSocial()"></div>
499 <div class="field"><div class="lbl"><span>Description (og:description)</span></div><textarea class="ctrl" name="_metasync_og_description" maxlength="155" placeholder="Post excerpt&hellip;" oninput="mssSocial()"><?php echo esc_textarea($og_desc); ?></textarea></div>
500 <div class="field"><div class="lbl"><span>Image (og:image)</span></div>
501 <div class="mss-imgrow"><input class="ctrl ogimg" type="url" name="_metasync_og_image" value="<?php echo $a($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>
502 <div class="hint">Recommended 1200&times;630px.</div></div>
503 <div class="row">
504 <div class="field"><div class="lbl"><span>Type (og:type)</span></div>
505 <select class="ctrl" name="_metasync_og_type">
506 <?php foreach (array('article', 'website', 'blog', 'product', 'video', 'music') as $t): ?>
507 <option value="<?php echo $a($t); ?>"<?php selected($og_type, $t); ?>><?php echo esc_html(ucfirst($t)); ?></option>
508 <?php endforeach; ?>
509 </select></div>
510 <div class="field"><div class="lbl"><span>URL (og:url)</span></div><input class="ctrl" type="url" name="_metasync_og_url" value="<?php echo $a($og_url); ?>" placeholder="<?php echo $a($pretty); ?>"<?php echo $post->post_status === 'auto-draft' ? ' disabled' : ''; ?>></div>
511 </div>
512
513 <div class="subhead"><span class="sq"></span> Twitter Card</div>
514 <div class="field"><div class="lbl"><span>Card Type</span></div>
515 <select class="ctrl mssTw" name="_metasync_twitter_card" onchange="mssTwT(this)">
516 <option value="summary"<?php selected($tw_card, 'summary'); ?>>Summary</option>
517 <option value="summary_large_image"<?php selected($tw_card, 'summary_large_image'); ?>>Summary Large Image</option>
518 <option value="app"<?php selected($tw_card, 'app'); ?>>App</option>
519 <option value="player"<?php selected($tw_card, 'player'); ?>>Player</option>
520 </select></div>
521 <div class="row">
522 <div class="field"><div class="lbl"><span>Twitter Site</span></div><input class="ctrl" name="_metasync_twitter_site" value="<?php echo $a($tw_site); ?>" placeholder="@yoursite"></div>
523 <div class="field"><div class="lbl"><span>Twitter Title</span></div><input class="ctrl" name="_metasync_twitter_title" maxlength="70" value="<?php echo $a($tw_title); ?>" placeholder="Falls back to OG title" oninput="mssSocial()"></div>
524 </div>
525 <div class="field"><div class="lbl"><span>Twitter Description</span></div><textarea class="ctrl" name="_metasync_twitter_description" maxlength="200" placeholder="Falls back to OG description" oninput="mssSocial()"><?php echo esc_textarea($tw_desc); ?></textarea></div>
526 <div class="row">
527 <div class="field"><div class="lbl"><span>Twitter Image</span></div>
528 <div class="mss-imgrow"><input class="ctrl twimg" type="url" name="_metasync_twitter_image" value="<?php echo $a($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>
529 <div class="field"><div class="lbl"><span>Image Alt</span></div><input class="ctrl" name="_metasync_twitter_image_alt" maxlength="420" value="<?php echo $a($tw_alt); ?>" placeholder="Accessibility description"></div>
530 </div>
531
532 <div class="mssTwApp" style="display:<?php echo $tw_card === 'app' ? 'block' : 'none'; ?>">
533 <div class="subhead"><span class="sq"></span> App Card</div>
534 <div class="row">
535 <div class="field"><div class="lbl"><span>iPhone App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_iphone" value="<?php echo $a($tw_app_iphone); ?>" placeholder="307234931"<?php echo $tw_app_disabled; ?>></div>
536 <div class="field"><div class="lbl"><span>iPad App ID</span></div><input class="ctrl" name="_metasync_twitter_app_id_ipad" value="<?php echo $a($tw_app_ipad); ?>" placeholder="307234931"<?php echo $tw_app_disabled; ?>></div>
537 </div>
538 <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 $a($tw_app_gp); ?>" placeholder="com.android.app"<?php echo $tw_app_disabled; ?>></div>
539 <div class="row">
540 <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 $a($tw_app_url_iphone); ?>" placeholder="myapp://"<?php echo $tw_app_disabled; ?>></div>
541 <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 $a($tw_app_url_ipad); ?>" placeholder="myapp://"<?php echo $tw_app_disabled; ?>></div>
542 </div>
543 <div class="row">
544 <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 $a($tw_app_url_gp); ?>" placeholder="myapp://"<?php echo $tw_app_disabled; ?>></div>
545 <div class="field"><div class="lbl"><span>App Store Country</span></div>
546 <select class="ctrl" name="_metasync_twitter_app_country"<?php echo $tw_app_disabled; ?>>
547 <option value=""<?php selected($tw_app_country, ''); ?>>US (Default)</option>
548 <?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): ?>
549 <option value="<?php echo $a($code); ?>"<?php selected($tw_app_country, $code); ?>><?php echo esc_html($country); ?></option>
550 <?php endforeach; ?>
551 </select>
552 <div class="hint">Required if your app is not available in the US App Store.</div>
553 </div>
554 </div>
555 </div>
556 <div class="mssTwPlayer" style="display:<?php echo $tw_card === 'player' ? 'block' : 'none'; ?>">
557 <div class="subhead"><span class="sq"></span> Player Card</div>
558 <div class="field"><div class="lbl"><span>Player URL</span></div><input class="ctrl" type="url" name="_metasync_twitter_player" value="<?php echo $a($tw_player); ?>" placeholder="https://example.com/player"<?php echo $tw_player_disabled; ?>></div>
559 <div class="row">
560 <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 $a($tw_player_w); ?>" placeholder="1280"<?php echo $tw_player_disabled; ?>></div>
561 <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 $a($tw_player_h); ?>" placeholder="720"<?php echo $tw_player_disabled; ?>></div>
562 </div>
563 </div>
564
565 <div class="subhead"><span class="sq"></span> Social preview</div>
566 <div class="mss-sptabs">
567 <button type="button" class="on" onclick="mssSpTab(this,'facebook')">Facebook</button>
568 <button type="button" onclick="mssSpTab(this,'twitter')">Twitter / X</button>
569 <button type="button" onclick="mssSpTab(this,'linkedin')">LinkedIn</button>
570 </div>
571 <div class="spwrap">
572 <div class="spcard fb on" data-plat="facebook">
573 <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>
574 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
575 <div class="sp-body">
576 <div class="sp-url"><?php echo esc_html($host); ?></div>
577 <div class="sp-title spT"></div>
578 <div class="sp-desc spD"></div>
579 </div>
580 </div>
581 <div class="spcard tw" data-plat="twitter">
582 <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>
583 <div class="tw-card">
584 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
585 <div class="sp-body">
586 <div class="sp-title spT"></div>
587 <div class="sp-desc spD"></div>
588 <div class="sp-url"><?php echo esc_html($host_lc); ?></div>
589 </div>
590 </div>
591 </div>
592 <div class="spcard li" data-plat="linkedin">
593 <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>
594 <div class="sp-img"><span class="sp-noimg">No image selected</span></div>
595 <div class="sp-body">
596 <div class="sp-title spT"></div>
597 <div class="sp-url"><?php echo esc_html($host_lc); ?> &middot; 1 min read</div>
598 </div>
599 </div>
600 </div>
601 </div>
602 <?php endif; ?>
603
604 <?php if ($has_schema): ?>
605 <div class="<?php echo $pcls(); ?>" data-p="schema">
606 <h2 class="p-title">&#128208; Schema Markup</h2>
607 <div class="mss-native">
608 <?php
609 if (!empty($this->schema_box['callback'])) {
610 call_user_func($this->schema_box['callback'], $post, $this->schema_box);
611 } else {
612 echo '<p class="p-sub">Schema builder unavailable on this screen.</p>';
613 }
614 ?>
615 </div>
616 </div>
617 <?php endif; ?>
618
619 <?php if ($has_video): ?>
620 <div class="<?php echo $pcls(); ?>" data-p="video">
621 <h2 class="p-title">&#127916; Video Sitemap</h2>
622 <p class="p-sub">Override auto-detected video data. Leave blank to use auto-detection.</p>
623 <div class="field"><div class="lbl"><span>Video URL</span></div><input class="ctrl" type="url" name="metasync_video_url" value="<?php echo $a($v_url); ?>" placeholder="https://www.youtube.com/watch?v=..."></div>
624 <div class="field"><div class="lbl"><span>Thumbnail URL</span></div><input class="ctrl" type="url" name="metasync_video_thumbnail" value="<?php echo $a($v_thumb); ?>" placeholder="https://img.youtube.com/vi/.../hqdefault.jpg"></div>
625 <div class="field"><div class="lbl"><span>Video Title</span></div><input class="ctrl" name="metasync_video_title" value="<?php echo $a($v_title); ?>" placeholder="Defaults to post title"></div>
626 <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>
627 <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 $a($v_dur); ?>" placeholder="300"></div>
628 </div>
629 <?php endif; ?>
630
631 </div>
632 </div>
633
634 <div class="mb-foot">
635 <span class="mss-foot-hint">Changes are saved when you update the post.</span>
636 </div>
637
638 <?php if ($managed): ?>
639 <div class="mss-lps-overlay">
640 <div class="mss-lps-card">
641 <div class="mss-lps-icon">&#128274;</div>
642 <h3>SEO managed by WebStudio</h3>
643 <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>
644 <div class="mss-lps-hint">Editing is disabled here to avoid conflicting values.</div>
645 </div>
646 </div>
647 <?php endif; ?>
648 </div>
649 </div>
650 </div>
651
652 <script>
653 (function(){
654 var root=document.getElementById('mss-root'); if(!root) return;
655 function byName(n){return root.querySelector('[name="'+n+'"]');}
656 function mssFld(n){var e=byName(n);return e?e.value.trim():'';}
657 window.mssKey=function(e,el){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); el.click(); } };
658 window.mssGo=function(el,p){
659 el.parentElement.querySelectorAll('.nav').forEach(function(n){n.classList.remove('active')});
660 el.classList.add('active');
661 root.querySelectorAll('.panel').forEach(function(pn){pn.classList.remove('active')});
662 var t=root.querySelector('.panel[data-p="'+p+'"]'); if(t)t.classList.add('active');
663 };
664 window.mssRc=function(){
665 var ti=root.querySelector('.seoT'), de=root.querySelector('.seoD'); if(!ti) return;
666 root.querySelector('.tC').textContent=ti.value.length;
667 root.querySelector('.dC').textContent=de.value.length;
668 root.querySelector('.gT').textContent=ti.value||ti.getAttribute('placeholder')||'Untitled';
669 root.querySelector('.gD').textContent=de.value||de.getAttribute('placeholder')||'No description set.';
670 };
671 window.mssTwT=function(sel){
672 var isApp=sel.value==='app', isPlayer=sel.value==='player';
673 var appSection=root.querySelector('.mssTwApp'), playerSection=root.querySelector('.mssTwPlayer');
674 if(appSection){
675 appSection.style.display=isApp?'block':'none';
676 appSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isApp;});
677 }
678 if(playerSection){
679 playerSection.style.display=isPlayer?'block':'none';
680 playerSection.querySelectorAll('input,select,textarea').forEach(function(field){field.disabled=!isPlayer;});
681 }
682 };
683 window.mssTheme=function(btn){
684 var dark=root.getAttribute('data-theme')==='dark';
685 root.setAttribute('data-theme',dark?'light':'dark');
686 btn.querySelector('.ti').textContent=dark?'\u{1F319}':'☀️';
687 btn.querySelector('.tl').textContent=dark?'Dark':'Light';
688 };
689 window.mssSpTab=function(btn,p){
690 btn.parentElement.querySelectorAll('button').forEach(function(b){b.classList.remove('on')});
691 btn.classList.add('on');
692 root.querySelectorAll('.spcard').forEach(function(c){c.classList.toggle('on', c.getAttribute('data-plat')===p)});
693 };
694 window.mssSocial=function(){
695 var seoT=root.querySelector('.seoT'), seoD=root.querySelector('.seoD');
696 var ogTitleEl=byName('_metasync_og_title');
697 var baseTitle=mssFld('_metasync_og_title') || (ogTitleEl&&ogTitleEl.getAttribute('placeholder')) || (seoT&&seoT.value.trim()) || 'Untitled';
698 var baseDesc =mssFld('_metasync_og_description') || (seoD&&seoD.value.trim()) || 'No description set.';
699 var baseImg =mssFld('_metasync_og_image');
700 var twTitle=mssFld('_metasync_twitter_title');
701 var twDesc =mssFld('_metasync_twitter_description');
702 var twImg =mssFld('_metasync_twitter_image');
703 root.querySelectorAll('.spcard').forEach(function(c){
704 var isTw=c.getAttribute('data-plat')==='twitter';
705 var T=isTw?(twTitle||baseTitle):baseTitle;
706 var D=isTw?(twDesc||baseDesc):baseDesc;
707 var I=isTw?(twImg||baseImg):baseImg;
708 var et=c.querySelector('.spT'); if(et)et.textContent=T;
709 var ed=c.querySelector('.spD'); if(ed)ed.textContent=D;
710 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');} }
711 });
712 };
713 root.querySelectorAll('.mss-pick').forEach(function(btn){
714 btn.addEventListener('click',function(e){e.preventDefault();if(typeof wp==='undefined'||!wp.media)return;
715 var target=root.querySelector(btn.getAttribute('data-target'));
716 var frame=wp.media({title:'Select image',button:{text:'Use image'},multiple:false});
717 frame.on('select',function(){var img=frame.state().get('selection').first().toJSON();if(target){target.value=img.url;mssSocial();}});
718 frame.open();});
719 });
720 ['_metasync_og_title','_metasync_og_description','_metasync_og_image','_metasync_twitter_title','_metasync_twitter_description','_metasync_twitter_image'].forEach(function(n){
721 var e=byName(n); if(e) e.addEventListener('input',mssSocial);
722 });
723 var twitterCard=byName('_metasync_twitter_card'); if(twitterCard)mssTwT(twitterCard); mssRc(); mssSocial();
724 })();
725 </script>
726 <?php
727 }
728
729 private function styles()
730 {
731 static $done = false;
732 if ($done) {
733 return;
734 }
735 $done = true;
736 ?>
737 <style>
738 #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)}
739 #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)}
740 #mss-root,#mss-root *{box-sizing:border-box}
741 #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}
742 #mss-root .mb-head{display:flex;align-items:center;gap:12px;padding:16px 20px;border-bottom:1px solid var(--border-soft)}
743 #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}
744 #mss-root .mb-head .t b{font-size:14.5px;color:var(--text)}
745 #mss-root .mb-head .t span{display:block;color:var(--muted);font-size:11.5px}
746 #mss-root .mss-actions{margin-left:auto;display:flex;gap:8px}
747 #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}
748 #mss-root .mb-body{display:grid;grid-template-columns:212px 1fr;min-height:430px}
749 @media (max-width:900px){#mss-root .mb-body{grid-template-columns:1fr}}
750 #mss-root .rail{background:var(--rail);border-right:1px solid var(--border-soft);padding:12px 10px;display:flex;flex-direction:column;gap:3px}
751 @media (max-width:900px){#mss-root .rail{flex-direction:row;overflow-x:auto;border-right:0;border-bottom:1px solid var(--border-soft)}}
752 #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}
753 #mss-root .nav:hover{background:var(--card-2);color:var(--text)}
754 #mss-root .nav.active{background:var(--grad-soft);color:var(--accent);border-color:rgba(102,126,234,.35)}
755 #mss-root .nav.active::before{content:"";position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:3px;background:var(--grad)}
756 #mss-root .nav .ic{width:20px;height:20px;flex:none;display:flex;align-items:center;justify-content:center;font-size:14px}
757 #mss-root .panel-wrap{padding:24px 26px;position:relative}
758 #mss-root .panel{display:none;animation:mssfade .22s ease}
759 #mss-root .panel.active{display:block}
760 @keyframes mssfade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
761 #mss-root .p-title{font-size:16px;font-weight:700;margin:0 0 3px;display:flex;align-items:center;gap:9px;color:var(--text)}
762 #mss-root .p-sub{color:var(--muted);font-size:12.5px;margin:0 0 20px}
763 #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}
764 #mss-root .sec::after{content:"";flex:1;height:1px;background:var(--border-soft)}
765 #mss-root .sec-note{text-transform:none;letter-spacing:0;font-weight:500;color:var(--faint)}
766 #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)}
767 #mss-root .subhead .sq{width:9px;height:16px;border-radius:3px;background:var(--grad);flex:none}
768 #mss-root .field{margin:0 0 17px}
769 #mss-root .field:last-child{margin-bottom:0}
770 #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)}
771 #mss-root .field .lbl .cc{color:var(--faint);font-weight:600;font-size:11px}
772 #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}
773 #mss-root .ctrl:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(102,126,234,.16)}
774 #mss-root textarea.ctrl{min-height:78px;resize:vertical}
775 #mss-root input.short{max-width:140px}
776 #mss-root .mss-imgrow{display:flex;gap:9px}
777 #mss-root .hint{color:var(--faint);font-size:11.5px;margin-top:5px;line-height:1.5}
778 #mss-root .row{display:grid;grid-template-columns:1fr 1fr;gap:15px}
779 @media (max-width:560px){#mss-root .row{grid-template-columns:1fr}}
780 #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}
781 #mss-root .switch{width:42px;height:24px;border-radius:24px;background:var(--border);position:relative;flex:none;cursor:pointer;transition:.18s;display:inline-block}
782 #mss-root .switch input{position:absolute;opacity:0;width:0;height:0}
783 #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)}
784 #mss-root .switch:has(input:checked){background:var(--grad)}
785 #mss-root .switch:has(input:checked)::after{left:21px}
786 #mss-root .switch-card .m b{display:block;font-size:13px;color:var(--text)}
787 #mss-root .switch-card .m span{color:var(--muted);font-size:11.5px}
788 #mss-root .chips{display:flex;flex-wrap:wrap;gap:9px}
789 #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)}
790 #mss-root .chip input{position:absolute;opacity:0;width:0;height:0}
791 #mss-root .chip:hover{border-color:var(--accent)}
792 #mss-root .chip:has(input:checked){background:var(--grad-soft);border-color:var(--accent);color:var(--accent)}
793 #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}
794 #mss-root .chip:has(input:checked) .box{background:var(--accent);border-color:var(--accent);color:#fff}
795 #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}
796 #mss-root .chip:has(input:checked) small{color:var(--text)}
797 #mss-root .mss-advchip{display:inline-flex;margin-bottom:7px}
798 #mss-root .gprev{border:1px solid var(--border);border-radius:11px;padding:15px 17px;background:var(--field-bg)}
799 #mss-root .gprev .u{color:var(--muted);font-size:12px}
800 #mss-root .gprev .ti{color:#1a0dab;font-size:18px;margin:3px 0;font-weight:400}
801 #mss-root[data-theme="dark"] .gprev .ti{color:#8ab4f8}
802 #mss-root .gprev .de{color:var(--muted);font-size:12.5px}
803 #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}
804 #mss-root .btn:hover{background:var(--grad-hover);color:#fff}
805 #mss-root .btn.ghost{background:transparent;color:var(--accent);border:1.5px solid rgba(102,126,234,.45);box-shadow:none}
806 #mss-root .btn.ghost:hover{background:var(--grad-soft)}
807 #mss-root .btn.sm{padding:6px 12px;font-size:12px}
808 #mss-root .mss-sptabs{display:flex;gap:2px;margin-bottom:13px;border-bottom:1px solid var(--border-soft)}
809 #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}
810 #mss-root .mss-sptabs button.on{color:var(--accent);border-bottom-color:var(--accent)}
811 #mss-root .spcard{display:none;border:1px solid var(--border);border-radius:11px;overflow:hidden;background:var(--field-bg);max-width:524px}
812 #mss-root .spcard.on{display:block}
813 #mss-root .sp-head{display:flex;align-items:center;gap:10px;padding:11px 13px}
814 #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}
815 #mss-root .sp-meta b{font-size:13px;color:var(--text);display:block;line-height:1.3}
816 #mss-root .sp-meta span{font-size:11px;color:var(--faint)}
817 #mss-root .sp-img{height:210px;background:var(--card-2) center/cover no-repeat;display:flex;align-items:center;justify-content:center}
818 #mss-root .sp-img.has .sp-noimg{display:none}
819 #mss-root .sp-noimg{color:var(--faint);font-size:12px}
820 #mss-root .sp-body{padding:11px 14px;border-top:1px solid var(--border-soft);background:var(--card-2)}
821 #mss-root[data-theme="dark"] .sp-body{background:var(--card)}
822 #mss-root .sp-url{font-size:10.5px;text-transform:uppercase;color:var(--faint);letter-spacing:.03em}
823 #mss-root .sp-title{font-size:15px;font-weight:700;color:var(--text);margin:3px 0}
824 #mss-root .sp-desc{font-size:12.5px;color:var(--muted)}
825 #mss-root .spcard.tw .tw-card{margin:0 13px 14px;border:1px solid var(--border);border-radius:15px;overflow:hidden}
826 #mss-root .spcard.tw .sp-body{border-top:0;background:var(--field-bg);display:flex;flex-direction:column}
827 #mss-root .spcard.tw .sp-title{font-size:14px;margin:0 0 3px;order:1}
828 #mss-root .spcard.tw .sp-desc{order:2}
829 #mss-root .spcard.tw .sp-url{order:0;text-transform:none;margin-bottom:4px}
830 #mss-root .spcard.li .sp-title{font-size:14.5px}
831 #mss-root .mb-foot{display:flex;align-items:center;gap:14px;padding:13px 26px;border-top:1px solid var(--border-soft);background:var(--rail)}
832 #mss-root .mss-foot-hint{font-size:12px;color:var(--muted)}
833 #mss-root .mss-lockable{position:relative}
834 #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)}
835 #mss-root[data-theme="dark"] .mss-lps-overlay{background:rgba(15,20,25,.66)}
836 #mss-root.lps .mss-lps-overlay{display:flex}
837 #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}
838 #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)}
839 #mss-root .mss-lps-card h3{margin:0 0 9px;font-size:18px;color:var(--text)}
840 #mss-root .mss-lps-card p{margin:0 0 20px;color:var(--muted);font-size:13px;line-height:1.65}
841 #mss-root .mss-lps-card p b{color:var(--text)}
842 #mss-root .mss-lps-card p code{background:var(--card-2);padding:1px 5px;border-radius:4px;font-size:11px}
843 #mss-root .mss-lps-hint{margin-top:15px;font-size:11.5px;color:var(--faint)}
844 #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}
845 #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}
846 #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}
847 /* The builder's own `.schema-field input {width:100%}` also hits checkboxes and radios,
848 stretching them into a full-width block. Core styles these with
849 -webkit-appearance:none, so they have NO intrinsic size and depend entirely on an
850 explicit width/height — restate core's 1rem box rather than using width:auto, which
851 would collapse the control to nothing while unchecked (the checked state only stays
852 visible because core paints a ::before glyph over it). */
853 #mss-root .mss-native input[type=checkbox],
854 #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}
855 #mss-root .mss-native input[type=radio]{border-radius:50% !important}
856 #mss-root .mss-native .schema-field label:has(input[type=checkbox]),
857 #mss-root .mss-native .schema-field label:has(input[type=radio]){display:flex;align-items:center;gap:2px}
858 /* Dark mode: theme the embedded native Schema builder. Every surface the builder paints
859 light — whether from its stylesheet or an inline style — is listed here. Text is only
860 forced light on surfaces this block also darkens, so nothing ends up light-on-light. */
861 #mss-root[data-theme="dark"] .mss-native{color:var(--text)}
862 #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}
863 #mss-root[data-theme="dark"] .mss-native .description,#mss-root[data-theme="dark"] .mss-native small{color:var(--muted) !important}
864 /* Light surfaces → dark. Class/ID list mirrors schema-markup-admin.css; the [style*=]
865 matchers catch the builder's inline backgrounds (#f5f5f5, #f9f9f9, #fff, white). */
866 #mss-root[data-theme="dark"] .mss-native .schema-types-container,
867 #mss-root[data-theme="dark"] .mss-native .schema-types-header,
868 #mss-root[data-theme="dark"] .mss-native .schema-type-item,
869 #mss-root[data-theme="dark"] .mss-native .schema-type-header,
870 #mss-root[data-theme="dark"] .mss-native .schema-type-content,
871 #mss-root[data-theme="dark"] .mss-native .schema-fields-container,
872 #mss-root[data-theme="dark"] .mss-native .schema-field,
873 #mss-root[data-theme="dark"] .mss-native .schema-field-group,
874 #mss-root[data-theme="dark"] .mss-native .schema-override-header,
875 #mss-root[data-theme="dark"] .mss-native .schema-override-content,
876 #mss-root[data-theme="dark"] .mss-native .no-schema-types,
877 #mss-root[data-theme="dark"] .mss-native .faq-item,
878 #mss-root[data-theme="dark"] .mss-native .instruction-item,
879 #mss-root[data-theme="dark"] .mss-native .logo-upload-container,
880 #mss-root[data-theme="dark"] .mss-native .logo-preview,
881 #mss-root[data-theme="dark"] .mss-native #schema-preview-output,
882 #mss-root[data-theme="dark"] .mss-native #schema-type-modal > div,
883 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff"],
884 #mss-root[data-theme="dark"] .mss-native [style*="background:#fff"],
885 #mss-root[data-theme="dark"] .mss-native [style*="background: white"],
886 #mss-root[data-theme="dark"] .mss-native [style*="background:white"],
887 #mss-root[data-theme="dark"] .mss-native [style*="background: #f9f9f9"],
888 #mss-root[data-theme="dark"] .mss-native [style*="background:#f9f9f9"],
889 #mss-root[data-theme="dark"] .mss-native [style*="background: #f5f5f5"],
890 #mss-root[data-theme="dark"] .mss-native [style*="background:#f5f5f5"],
891 #mss-root[data-theme="dark"] .mss-native [style*="#ffffff"]{background-color:var(--card-2) !important;border-color:var(--border) !important}
892 /* Notices keep their semantic colour instead of being flattened to a grey surface. */
893 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
894 #mss-root[data-theme="dark"] .mss-native [style*="background: #fff3cd"],
895 #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}
896 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning,
897 #mss-root[data-theme="dark"] .mss-native .metasync-schema-validation-warning *,
898 #mss-root[data-theme="dark"] .mss-native [style*="#fff3cd"] *,
899 #mss-root[data-theme="dark"] .mss-native [style*="color: #856404"],
900 #mss-root[data-theme="dark"] .mss-native [style*="color:#856404"]{color:#fcd34d !important}
901 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
902 #mss-root[data-theme="dark"] .mss-native [style*="background: #e7f3ff"],
903 #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}
904 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info,
905 #mss-root[data-theme="dark"] .mss-native .metasync-schema-requirements-info *{color:#c7d2fe !important}
906 /* Dark inline text on the surfaces darkened above → readable. */
907 #mss-root[data-theme="dark"] .mss-native [style*="color: #333"],
908 #mss-root[data-theme="dark"] .mss-native [style*="color:#333"],
909 #mss-root[data-theme="dark"] .mss-native [style*="color: #000"],
910 #mss-root[data-theme="dark"] .mss-native [style*="color:#000"],
911 #mss-root[data-theme="dark"] .mss-native [style*="color: #666"],
912 #mss-root[data-theme="dark"] .mss-native [style*="color:#666"]{color:var(--text) !important}
913 #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}
914 /* Checkboxes/radios: core paints them white-on-dark-border, which glares against this
915 box. Unchecked gets the field surface with a visible border; checked gets the brand
916 accent, which keeps enough contrast for core's white ::before tick. */
917 #mss-root[data-theme="dark"] .mss-native input[type=checkbox],
918 #mss-root[data-theme="dark"] .mss-native input[type=radio]{background:var(--field-bg) !important;border-color:#6b7280 !important}
919 #mss-root[data-theme="dark"] .mss-native input[type=checkbox]:checked,
920 #mss-root[data-theme="dark"] .mss-native input[type=radio]:checked{background:var(--accent) !important;border-color:var(--accent) !important}
921 /* Core's .button is painted light by wp-admin; keep it legible on the dark surface.
922 Destructive buttons (the builder paints them red) are left alone. */
923 #mss-root[data-theme="dark"] .mss-native .button,
924 #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}
925 #mss-root[data-theme="dark"] .mss-native .button:hover,
926 #mss-root[data-theme="dark"] .mss-native .button-secondary:hover{background:var(--grad-soft) !important;color:var(--text) !important}
927 #mss-root[data-theme="dark"] .mss-native .button-primary{background:var(--grad) !important;color:#fff !important;border-color:transparent !important}
928 /* .remove-schema-type is a text-style .button (red text, no fill) — keep it red rather
929 than letting the .button rule above recolour it. .remove-faq-item / .remove-item are
930 solid-red fills with white text and are deliberately left untouched. */
931 #mss-root[data-theme="dark"] .mss-native .remove-schema-type,
932 #mss-root[data-theme="dark"] .mss-native .remove-schema-type .dashicons{color:#f87171 !important;border-color:rgba(248,113,113,.45) !important}
933 </style>
934 <?php
935 }
936 }
937