{"id":2006,"date":"2026-07-19T11:24:59","date_gmt":"2026-07-19T09:24:59","guid":{"rendered":"https:\/\/keypers.io\/?page_id=2006"},"modified":"2026-07-21T11:38:28","modified_gmt":"2026-07-21T09:38:28","slug":"serp-simulator","status":"publish","type":"page","link":"https:\/\/keypers.io\/en\/serp-simulator\/","title":{"rendered":"SERP Simulator"},"content":{"rendered":"\t\t\t\t<div class=\"component-gutenberg-html prose prose-lg max-w-full\">\n\t\t\t\t\t<div class=\"container\">\n\t\t\t\t\t\t\t<div class=\"max-w-prose mx-auto\">\n\t\t\n<!-- ==================================================================\n     Keypers SERP simulator \u2014 WordPress embed build (ENGLISH \/ WPML \/en\/ version).\n\n     Paste this WHOLE block into a \"Vlastn\u00ed HTML\" (Custom HTML) block on the\n     page \u2014 not a Paragraph\/Classic block (that would let wpautop inject\n     <br>\/<p> and break it). One instance per page. After publishing, purge\n     the Breeze cache.\n\n     It is the standalone serp-simulator.html rewritten to live safely inside\n     the keypers.io theme:\n       \u2022 everything is wrapped in <div class=\"kp-serp\"> and every CSS rule is\n         scoped to it; every class is prefixed kp- and every id kp-, so the\n         tool cannot restyle the site and the theme cannot bleed into the tool;\n       \u2022 the tool gives itself a bit more width than the article column so the\n         form and the live Google preview sit SIDE BY SIDE (the preview is the\n         hero \u2014 it shouldn't be a scroll away). It stays centred and capped, and\n         on narrow screens it stacks with the preview leading;\n       \u2022 the layout breakpoint is a CONTAINER query, so it responds to the tool's\n         own width, not the browser window.\n     Design tokens and the light-only decision are unchanged \u2014 see the comments.\n     ================================================================== -->\n<div class=\"kp-serp\">\n<style>\n  \/* ------------------------------------------------------------------\n     Design tokens, lifted verbatim from the live Keypers theme:\n     keypers.io\/wp-content\/themes\/keypers\/static\/dist\/css\/style.min.css\n     Kept in oklch so the values match the source system exactly.\n\n     Single light theme on purpose. This drops into a white WordPress page on\n     keypers.io \u2014 a viewer-triggered dark flip would leave the tool inverted\n     inside a light site, which reads as broken rather than considered. Tokens\n     live on .kp-serp (not :root) so they inherit to our elements without\n     leaking to the rest of the page.\n     ------------------------------------------------------------------ *\/\n  .kp-serp {\n    color-scheme: light;\n\n    --kp-primary-50:  oklch(91.67% .0409 281.03);  \/* \u2248 #DEE1FF, the soft tint *\/\n    --kp-primary-100: oklch(84.3% .085 291.99);\n    --kp-primary-200: oklch(68.95% .174 288.5);\n    --kp-primary-300: oklch(55.25% .255 281.77);\n    --kp-primary-400: oklch(46.94% .302 270.62);\n    --kp-primary-500: oklch(39.19% .258 269.44);   \/* the ink *and* the fill *\/\n    --kp-logo:        oklch(42.59% .274 272.12);\n\n    --kp-teal-700:   oklch(51.1% .096 186.391);\n    --kp-orange-700: oklch(55.3% .195 38.402);\n    --kp-red-700:    oklch(50.5% .213 27.518);\n\n    \/* Booton is the Keypers brand face, served from the theme on keypers.io,\n       so this inherits the real font once embedded there; opened as a\n       standalone file it falls back to the system stack by design. *\/\n    --font-sans: \"Booton\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", monospace;\n\n    \/* Semantic surface tokens \u2014 components read only these.\n       keypers.io sets no black anywhere: body, headings and nav all compute\n       to primary-500, so indigo is the text colour, not just an accent. *\/\n    --bg: #fff;\n    --bg-panel: #fff;\n    --bg-sunken: oklch(97.8% .012 283);            \/* faint indigo wash, never grey *\/\n    --fg: var(--kp-primary-500);\n    \/* The brand ramp is built for display type: primary-400 is both lighter\n       *and* more saturated than 500, so it fights the reader as body text.\n       These two hold the brand hue but dial chroma back to something readable,\n       and both clear WCAG AA on white (measured 5.76:1 \/ 4.79:1). *\/\n    --fg-muted: oklch(52% .16 275);\n    --fg-faint: oklch(56% .12 278);\n    --line: var(--kp-primary-50);\n    --line-strong: var(--kp-primary-100);\n    --accent: var(--kp-primary-500);\n    --accent-fg: #fff;\n    --accent-soft: var(--kp-primary-50);\n    --ok: var(--kp-teal-700);\n    --warn: var(--kp-orange-700);\n    --over: var(--kp-red-700);\n    --bezel: var(--kp-primary-50);\n    --bezel-line: var(--kp-primary-100);\n    --shadow: 0 1px 2px oklch(39% .1 269 \/ .07), 0 10px 30px oklch(39% .1 269 \/ .10);\n\n    \/* Filled controls on keypers.io are full pills (radius 999999px). *\/\n    --pill: 999px;\n\n    \/* Base type, formerly on <body>. Now confined to the tool. *\/\n    color: var(--fg);\n    font-family: var(--font-sans);\n    font-size: 15px;\n    line-height: 1.55;\n    -webkit-font-smoothing: antialiased;\n\n    \/* Modest breakout: the article column on keypers.io is ~985px, which is\n       too narrow to put the form and the 666px preview side by side \u2014 so the\n       tool would always stack and the preview (the whole point) would sit\n       below the fold. Give the tool a touch more room, centred on the column\n       and capped so it never reaches the viewport edge or triggers a\n       horizontal scrollbar. `(100% - W)\/2` centres W on the parent column\n       (negative when W is wider), and `100vw - 24px` keeps a safe gutter. The\n       site already uses full-width sections (header, newsletter), so a wider\n       tool block reads as intentional, not as an escapee. *\/\n    --kp-w: min(1200px, calc(100vw - 24px));\n    width: var(--kp-w);\n    margin-inline: calc((100% - var(--kp-w)) \/ 2);\n    min-width: 320px;   \/* never collapse under a shrink-to-fit parent *\/\n\n    \/* Lay out against the tool's own width, so the stack breakpoint reacts to\n       the tool, not the browser window. *\/\n    container-type: inline-size;\n    container-name: kpserp;\n  }\n\n  .kp-serp *,\n  .kp-serp *::before,\n  .kp-serp *::after { box-sizing: border-box; }\n\n  \/* Visually-hidden but available to screen readers. *\/\n  .kp-serp .kp-sr-only {\n    position: absolute;\n    width: 1px; height: 1px;\n    padding: 0; margin: -1px;\n    overflow: hidden;\n    clip: rect(0, 0, 0, 0);\n    white-space: nowrap;\n    border: 0;\n  }\n\n  .kp-serp .kp-wrap {\n    max-width: 1240px;\n    margin: 0 auto;\n    padding: 8px 16px 16px;   \/* side padding so full-bleed hosts don't hug edges *\/\n  }\n\n  \/* ---------------- header ---------------- *\/\n  .kp-serp .kp-masthead {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: flex-end;\n    justify-content: space-between;\n    gap: 20px;\n    padding-bottom: 20px;\n    border-bottom: 1px solid var(--line);\n    margin-bottom: 32px;\n  }\n  .kp-serp h1 {\n    font-size: 20px;\n    font-weight: 600;\n    margin: 0;\n    color: var(--fg);\n    letter-spacing: -.01em;\n    text-wrap: balance;\n  }\n  .kp-serp .kp-tagline {\n    margin: 6px 0 0;\n    color: var(--fg-muted);\n    font-size: 14px;\n    max-width: 58ch;\n  }\n\n  \/* ---------------- device switch ---------------- *\/\n  .kp-serp .kp-switch {\n    display: inline-flex;\n    background: var(--bg-sunken);\n    border: 1px solid var(--line);\n    border-radius: var(--pill);\n    padding: 3px;\n    gap: 2px;\n  }\n  .kp-serp .kp-switch button {\n    appearance: none;\n    border: 0;\n    background: transparent;\n    color: var(--fg-muted);\n    font-family: inherit;\n    font-size: 13px;\n    font-weight: 500;\n    padding: 7px 18px;\n    border-radius: var(--pill);\n    cursor: pointer;\n    transition: background .15s, color .15s;\n  }\n  .kp-serp .kp-switch button:hover { color: var(--fg); }\n  .kp-serp .kp-switch button[aria-pressed=\"true\"] {\n    background: var(--accent);\n    color: var(--accent-fg);\n  }\n  .kp-serp .kp-switch button:focus-visible {\n    outline: 2px solid var(--accent);\n    outline-offset: 2px;\n  }\n\n  \/* ---------------- layout ---------------- *\/\n  .kp-serp .kp-cols {\n    display: grid;\n    grid-template-columns: minmax(0, 1fr) 666px;\n    gap: 48px;\n    align-items: start;\n  }\n  \/* Stack once the tool is narrower than ~1100px rather than letting the\n     preview shrink \u2014 a squeezed preview is a wrong preview. Container query:\n     watches .kp-serp, so it reflects the tool's own (possibly broken-out)\n     width. When stacked, the preview leads (order:-1) so the hero is still the\n     first thing you see; DOM\/tab order stays inputs-first for keyboard users. *\/\n  @container kpserp (max-width: 1100px) {\n    .kp-serp .kp-cols { grid-template-columns: minmax(0, 1fr); gap: 40px; }\n    .kp-serp .kp-preview-col { position: static !important; order: -1; }\n    \/* the skeletons are only there to fill the desktop two-column space;\n       stacked, they'd just bury the inputs, so drop them. *\/\n    .kp-serp .kp-skeletons { display: none; }\n  }\n  \/* Fallback for engines without container-query support: use the viewport. *\/\n  @supports not (container-type: inline-size) {\n    @media (max-width: 1100px) {\n      .kp-serp .kp-cols { grid-template-columns: minmax(0, 1fr); gap: 40px; }\n      .kp-serp .kp-preview-col { position: static !important; order: -1; }\n      .kp-serp .kp-skeletons { display: none; }\n    }\n  }\n\n  .kp-serp .kp-fields { display: flex; flex-direction: column; gap: 28px; }\n\n  .kp-serp .kp-field { display: flex; flex-direction: column; gap: 8px; }\n\n  .kp-serp .kp-field-head {\n    display: flex;\n    align-items: baseline;\n    justify-content: space-between;\n    gap: 12px;\n  }\n  .kp-serp label {\n    font-size: 13px;\n    font-weight: 600;\n    color: var(--fg);\n    letter-spacing: .02em;\n  }\n  .kp-serp .kp-label-hint {\n    font-weight: 400;\n    color: var(--fg-faint);\n    font-size: 12px;\n  }\n\n  \/* Scoped to .kp-field so this styles ONLY the three real form inputs, never\n     the decorative Google search field. box-shadow:none keeps a theme's own\n     input shadow from bleeding in. *\/\n  .kp-serp .kp-field input[type=\"text\"],\n  .kp-serp .kp-field textarea {\n    width: 100%;\n    font-family: inherit;\n    font-size: 14px;\n    line-height: 1.5;\n    color: var(--fg);\n    background: var(--bg-panel);\n    border: 1px solid var(--line-strong);\n    border-radius: 10px;\n    box-shadow: none;\n    padding: 11px 14px;\n    transition: border-color .15s, box-shadow .15s;\n  }\n  \/* 2px outline on focus (mirrors the switch) \u2014 a reliable, forced-colors-safe\n     indicator, not just a 1px border tint. *\/\n  .kp-serp .kp-field input[type=\"text\"]:focus,\n  .kp-serp .kp-field textarea:focus {\n    outline: 2px solid var(--accent);\n    outline-offset: 2px;\n    border-color: var(--accent);\n  }\n  .kp-serp .kp-field textarea { resize: vertical; min-height: 88px; }\n\n  \/* ---------------- the pixel ruler ----------------\n     The point of the whole tool: characters lie, pixels don't.\n     The track IS the limit. The tick is where Google cuts. *\/\n  .kp-serp .kp-meter { display: flex; flex-direction: column; gap: 6px; }\n\n  .kp-serp .kp-track {\n    position: relative;\n    height: 6px;\n    background: var(--bg-sunken);\n    border: 1px solid var(--line);\n    border-radius: 999px;\n    overflow: hidden;\n  }\n  .kp-serp .kp-fill {\n    position: absolute;\n    inset: 0 auto 0 0;\n    background: var(--ok);\n    border-radius: 999px;\n    transition: width .12s linear, background .2s;\n  }\n  .kp-serp .kp-fill[data-state=\"warn\"] { background: var(--warn); }\n  .kp-serp .kp-fill[data-state=\"over\"] { background: var(--over); }\n  .kp-serp .kp-tick {\n    position: absolute;\n    top: -3px;\n    bottom: -3px;\n    width: 1px;\n    background: var(--fg-faint);\n    opacity: .5;\n  }\n\n  .kp-serp .kp-readout {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 12px;\n    font-size: 12px;\n    color: var(--fg-muted);\n  }\n  .kp-serp .kp-nums {\n    font-family: var(--font-mono);\n    font-variant-numeric: tabular-nums;\n    font-size: 11.5px;\n    letter-spacing: -.01em;\n  }\n  .kp-serp .kp-nums b { color: var(--fg); font-weight: 600; }\n  .kp-serp .kp-nums .kp-sep { opacity: .45; }\n\n  \/* state pill \u2014 state readable as form, not just colour *\/\n  .kp-serp .kp-pill {\n    display: inline-flex;\n    align-items: center;\n    gap: 5px;\n    font-size: 11px;\n    font-weight: 600;\n    letter-spacing: .03em;\n    text-transform: uppercase;\n    padding: 2px 8px;\n    border-radius: 999px;\n    border: 1px solid currentColor;\n    white-space: nowrap;\n  }\n  .kp-serp .kp-pill::before {\n    content: \"\";\n    width: 5px; height: 5px;\n    border-radius: 50%;\n    background: currentColor;\n  }\n  .kp-serp .kp-pill[data-state=\"ok\"]   { color: var(--ok); }\n  .kp-serp .kp-pill[data-state=\"warn\"] { color: var(--warn); }\n  .kp-serp .kp-pill[data-state=\"over\"] { color: var(--over); }\n\n  \/* ---------------- preview ---------------- *\/\n  .kp-serp .kp-preview-col { position: sticky; top: 32px; }\n\n  .kp-serp .kp-preview-head {\n    display: flex;\n    align-items: baseline;\n    justify-content: space-between;\n    gap: 12px;\n    margin-bottom: 10px;\n  }\n  .kp-serp .kp-eyebrow {\n    font-size: 11px;\n    font-weight: 600;\n    letter-spacing: .12em;\n    text-transform: uppercase;\n    color: var(--fg-faint);\n  }\n\n  \/* A screen inside a bezel: the preview is Google's surface, not ours, so it\n     keeps its own light rendering. Frame hugs the screen so the mobile view\n     reads as a phone, not a desktop with empty space beside it. *\/\n  .kp-serp .kp-bezel {\n    background: var(--bezel);\n    border: 1px solid var(--bezel-line);\n    border-radius: 14px;\n    padding: 14px;\n    box-shadow: var(--shadow);\n    width: fit-content;\n    max-width: 100%;\n  }\n  .kp-serp .kp-screen {\n    background: #fff;\n    border-radius: 6px;\n    padding: 20px 18px;\n    overflow-x: auto;\n  }\n  .kp-serp .kp-screen[data-device=\"mobile\"] { padding: 16px 14px; }\n\n  \/* Everything below renders in Google's own type, never Booton. *\/\n  .kp-serp .kp-snippet { font-family: Arial, \"Liberation Sans\", sans-serif; }\n  \/* Fixed width on purpose: the SERP column is 600px, so the preview must be\n     600px too. Letting it shrink to the layout would show line breaks Google\n     never makes and quietly contradict the pixel readout. The page (search bar\n     + snippet + skeletons) holds that fixed width; narrow screens scroll the\n     screen instead. *\/\n  .kp-serp .kp-page { width: 600px; }\n  .kp-serp .kp-screen[data-device=\"mobile\"] .kp-page { width: 400px; }\n  .kp-serp .kp-snippet-inner { width: 100%; }\n\n  .kp-serp .kp-snippet-site {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    margin-bottom: 4px;\n  }\n  .kp-serp .kp-snippet-favicon {\n    width: 26px; height: 26px;\n    border-radius: 50%;\n    background: #f1f3f4;\n    border: 1px solid #ecedef;\n    display: grid;\n    place-items: center;\n    font-size: 12px;\n    font-weight: 700;\n    color: #5f6368;\n    flex: none;\n  }\n  .kp-serp .kp-snippet-site-meta { min-width: 0; line-height: 1.2; }\n  .kp-serp .kp-snippet-site-name {\n    font-size: 14px;\n    color: #202124;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .kp-serp .kp-snippet-url {\n    font-size: 12px;\n    color: #4d5156;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .kp-serp .kp-snippet-title {\n    font-size: 20px;\n    line-height: 1.3;\n    color: #1a0dab;\n    margin: 2px 0 3px;\n    cursor: pointer;\n    word-break: break-word;\n  }\n  .kp-serp .kp-screen[data-device=\"mobile\"] .kp-snippet-title { font-size: 18px; }\n  .kp-serp .kp-snippet-title:hover { text-decoration: underline; }\n  .kp-serp .kp-snippet-desc {\n    font-size: 14px;\n    line-height: 1.58;\n    color: #4d5156;\n    word-break: break-word;\n  }\n\n  \/* Truncated text is shown, not deleted \u2014 you see what you're losing. *\/\n  .kp-serp .kp-ghost {\n    color: #bdc1c6;\n    background: repeating-linear-gradient(\n      -45deg,\n      transparent 0 3px,\n      oklch(90% .02 25 \/ .35) 3px 5px\n    );\n    border-radius: 2px;\n  }\n  .kp-serp .kp-snippet-empty { color: #5f6368; font-style: italic; }\n\n  .kp-serp .kp-cut-note {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    margin-top: 12px;\n    font-size: 12px;\n    color: var(--fg-muted);\n  }\n  .kp-serp .kp-cut-swatch {\n    width: 22px; height: 9px;\n    border-radius: 2px;\n    background: repeating-linear-gradient(\n      -45deg, #eceff1 0 3px, oklch(80% .06 25 \/ .5) 3px 5px\n    );\n    border: 1px solid var(--line);\n    flex: none;\n  }\n\n  \/* ---------------- Google chrome (decorative results-page mock) ----------------\n     Frames the live snippet as the top result on a mock Google page: a\n     non-functional search bar and tabs on top, greyed \"still loading\" skeleton\n     results below. Everything here is decoration; the skeletons hide when the\n     layout stacks so they don't bury the inputs on a phone. *\/\n  .kp-serp .kp-gbar {\n    display: flex;\n    align-items: center;\n    gap: 20px;\n    padding-bottom: 14px;\n  }\n  \/* Google's logo uses the proprietary \"Product Sans\", which can't be embedded;\n     this stack prefers geometric sans-serifs (a closer match where installed)\n     and falls back gracefully. For a pixel-exact logo, swap in Google's own\n     logo image. *\/\n  .kp-serp .kp-glogo {\n    font-family: \"Product Sans\", \"Google Sans\", Futura, \"Century Gothic\", \"Trebuchet MS\", system-ui, Arial, sans-serif;\n    font-size: 22px;\n    font-weight: 400;\n    letter-spacing: -.5px;\n    flex: none;\n    user-select: none;\n  }\n  .kp-serp .kp-glogo span:nth-child(1) { color: #4285F4; }\n  .kp-serp .kp-glogo span:nth-child(2) { color: #EA4335; }\n  .kp-serp .kp-glogo span:nth-child(3) { color: #FBBC05; }\n  .kp-serp .kp-glogo span:nth-child(4) { color: #4285F4; }\n  .kp-serp .kp-glogo span:nth-child(5) { color: #34A853; }\n  .kp-serp .kp-glogo span:nth-child(6) { color: #EA4335; }\n\n  .kp-serp .kp-gsearch {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    flex: 1;\n    min-width: 0;\n    border: 1px solid #dfe1e5;\n    border-radius: 24px;\n    padding: 8px 16px;\n    box-shadow: 0 1px 4px rgba(32, 33, 36, .08);\n  }\n  .kp-serp .kp-gmag { flex: none; }\n  \/* Fully reset so no theme input styling (border, radius, shadow, native\n     appearance) can draw a second \"box\" inside the search pill. *\/\n  .kp-serp .kp-ginput {\n    flex: 1;\n    min-width: 0;\n    border: 0;\n    border-radius: 0;\n    outline: 0;\n    box-shadow: none;\n    background: transparent;\n    -webkit-appearance: none;\n    appearance: none;\n    margin: 0;\n    padding: 0;\n    font-family: Arial, sans-serif;\n    font-size: 14px;\n    color: #202124;\n  }\n  .kp-serp .kp-ginput::placeholder { color: #9aa0a6; }\n\n  .kp-serp .kp-gtabs {\n    display: flex;\n    gap: 22px;\n    font-family: Arial, sans-serif;\n    font-size: 12px;\n    color: #5f6368;\n    border-bottom: 1px solid #ebebeb;\n    margin-bottom: 6px;\n  }\n  .kp-serp .kp-gtab { padding-bottom: 8px; }\n  .kp-serp .kp-gtab--on {\n    color: #1a73e8;\n    box-shadow: inset 0 -3px 0 #1a73e8;   \/* Google's blue active-tab underline *\/\n  }\n\n  \/* the live snippet, softly highlighted as the top result. The tint sits on a\n     pseudo-element so it can pad and round without changing the 600px text\n     width the preview depends on. *\/\n  .kp-serp .kp-snippet--yours { position: relative; margin: 2px 0 8px; }\n  .kp-serp .kp-snippet--yours::before {\n    content: \"\";\n    position: absolute;\n    inset: -10px -14px;\n    background: rgba(34, 0, 204, .05);\n    border-radius: 12px;\n    z-index: 0;\n  }\n  .kp-serp .kp-snippet--yours > * { position: relative; z-index: 1; }\n  .kp-serp .kp-yours-tag {\n    position: absolute;\n    top: 0;\n    right: 0;\n    font-family: var(--font-sans);\n    font-size: 10px;\n    font-weight: 600;\n    letter-spacing: .04em;\n    text-transform: uppercase;\n    color: var(--accent);\n    opacity: .8;\n  }\n\n  \/* skeleton \"still loading\" results *\/\n  .kp-serp .kp-skel { margin-top: 24px; }\n  .kp-serp .kp-skel-site {\n    display: flex; align-items: center; gap: 12px; margin-bottom: 10px;\n  }\n  .kp-serp .kp-skel-circle {\n    width: 26px; height: 26px; border-radius: 50%; background: #e5e7ea; flex: none;\n  }\n  .kp-serp .kp-skel-line { height: 12px; width: 150px; border-radius: 4px; background: #e5e7ea; }\n  .kp-serp .kp-skel-title {\n    height: 18px; width: 62%; border-radius: 4px;\n    background: rgba(34, 0, 204, .13); margin-bottom: 10px;\n  }\n  .kp-serp .kp-skel-desc { height: 40px; border-radius: 4px; background: #e5e7ea; }\n  \/* subtle \"loading\" pulse; the global prefers-reduced-motion rule disables it *\/\n  @keyframes kp-shimmer { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }\n  .kp-serp .kp-skel-circle,\n  .kp-serp .kp-skel-line,\n  .kp-serp .kp-skel-title,\n  .kp-serp .kp-skel-desc { animation: kp-shimmer 1.6s ease-in-out infinite; }\n  .kp-serp .kp-skel:nth-child(2) .kp-skel-title { animation-delay: .15s; }\n  .kp-serp .kp-skel:nth-child(3) .kp-skel-title { animation-delay: .3s; }\n  .kp-serp .kp-skel:nth-child(4) .kp-skel-title { animation-delay: .45s; }\n\n  \/* ---------------- caveats ---------------- *\/\n  .kp-serp .kp-caveats {\n    margin-top: 44px;\n    padding-top: 24px;\n    border-top: 1px solid var(--line);\n    display: grid;\n    \/* min(100%,260px) lets a track shrink below 260px on tiny screens instead\n       of forcing the whole page to scroll sideways. *\/\n    grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));\n    gap: 24px 40px;\n  }\n  .kp-serp .kp-caveat h2 {\n    font-size: 12px;\n    font-weight: 600;\n    letter-spacing: .08em;\n    text-transform: uppercase;\n    color: var(--fg-faint);\n    margin: 0 0 6px;\n  }\n  .kp-serp .kp-caveat p {\n    margin: 0;\n    font-size: 13px;\n    line-height: 1.6;\n    color: var(--fg-muted);\n    max-width: 60ch;\n  }\n  \/* ---------------- closing CTA ----------------\n     The tool used to dead-end and the visitor left. This is the one link from\n     the tool back to the service. Deliberately quiet: it sits after the\n     caveats, names the limit of the tool, and offers what lies beyond it.\n     Same tokens as everything else, no new colour. *\/\n  .kp-serp .kp-cta {\n    margin-top: 40px;\n    padding: 26px 30px;\n    background: var(--bg-sunken);\n    border: 1px solid var(--line);\n    border-radius: 14px;\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: space-between;\n    gap: 18px 32px;\n  }\n  .kp-serp .kp-cta-text { max-width: 62ch; }\n  .kp-serp .kp-cta h2 {\n    font-size: 15px;\n    font-weight: 600;\n    color: var(--fg);\n    margin: 0 0 4px;\n  }\n  .kp-serp .kp-cta p {\n    margin: 0;\n    font-size: 13px;\n    line-height: 1.6;\n    color: var(--fg-muted);\n  }\n  .kp-serp .kp-cta-btn {\n    flex: none;\n    display: inline-block;\n    background: var(--accent);\n    color: var(--accent-fg);\n    font-size: 14px;\n    font-weight: 500;\n    text-decoration: none;\n    padding: 11px 26px;\n    border-radius: var(--pill);\n    transition: opacity .15s;\n  }\n  .kp-serp .kp-cta-btn:hover { opacity: .88; color: var(--accent-fg); }\n  .kp-serp .kp-cta-btn:focus-visible {\n    outline: 2px solid var(--accent);\n    outline-offset: 2px;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .kp-serp *,\n    .kp-serp *::before,\n    .kp-serp *::after { transition: none !important; animation: none !important; }\n  }\n<\/style>\n\n<div class=\"kp-wrap\">\n  <header class=\"kp-masthead\">\n    <div>\n      <!-- No logo here on purpose: this page lives inside the keypers.io theme,\n           which already renders the site header. A second Keypers mark would\n           read as a widget dropped in rather than a native page. The tool keeps\n           only its functional chrome \u2014 a heading, one line of instructions, and\n           the device switch. This h1 is the page's heading; if the theme ever\n           starts printing its own visible page title, demote this to <h2>. -->\n      <h1>SERP simulator<\/h1>\n      <p class=\"kp-tagline\">\n        Type a&nbsp;title and description and instantly see how they\u2019ll appear in\n        Google\u2019s search results, and where they get cut&nbsp;off.&nbsp;<\/p>\n    <\/div>\n    <div class=\"kp-switch\" role=\"group\" aria-label=\"Device\">\n      <button type=\"button\" id=\"kp-btn-desktop\" aria-pressed=\"true\">Desktop<\/button>\n      <button type=\"button\" id=\"kp-btn-mobile\" aria-pressed=\"false\">Mobile<\/button>\n    <\/div>\n  <\/header>\n\n  <div class=\"kp-cols\">\n    <!-- ---------------- inputs ---------------- -->\n    <div class=\"kp-fields\">\n      <div class=\"kp-field\">\n        <div class=\"kp-field-head\">\n          <label for=\"kp-in-title\">Page title<\/label>\n          <span class=\"kp-label-hint\" id=\"kp-hint-title\">&lt;title&gt;<\/span>\n        <\/div>\n        <!-- Caps are far above any real title\/description. They exist so a\n             stray paste of a whole document can't stall the tab: every\n             keystroke re-measures the full string on a canvas. -->\n        <input type=\"text\" id=\"kp-in-title\" spellcheck=\"false\" maxlength=\"500\" aria-describedby=\"kp-hint-title kp-nums-title kp-pill-title\" value=\"SEO audit for e-shops: find what's slowing you down | Keypers\">\n        <div class=\"kp-meter\">\n          <div class=\"kp-track\" id=\"kp-track-title\">\n            <div class=\"kp-fill\" id=\"kp-fill-title\"><\/div>\n            <div class=\"kp-tick\" id=\"kp-tick-title\"><\/div>\n          <\/div>\n          <div class=\"kp-readout\">\n            <span class=\"kp-nums\" id=\"kp-nums-title\"><\/span>\n            <span class=\"kp-pill\" id=\"kp-pill-title\"><\/span>\n          <\/div>\n        <\/div>\n        <span class=\"kp-sr-only\" id=\"kp-status-title\" role=\"status\" aria-live=\"polite\"><\/span>\n      <\/div>\n\n      <div class=\"kp-field\">\n        <div class=\"kp-field-head\">\n          <label for=\"kp-in-desc\">Description<\/label>\n          <span class=\"kp-label-hint\" id=\"kp-hint-desc\">meta description<\/span>\n        <\/div>\n        <textarea id=\"kp-in-desc\" spellcheck=\"false\" maxlength=\"2000\" aria-describedby=\"kp-hint-desc kp-nums-desc kp-pill-desc\">We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients.<\/textarea>\n        <div class=\"kp-meter\">\n          <div class=\"kp-track\" id=\"kp-track-desc\">\n            <div class=\"kp-fill\" id=\"kp-fill-desc\"><\/div>\n            <div class=\"kp-tick\" id=\"kp-tick-desc\"><\/div>\n          <\/div>\n          <div class=\"kp-readout\">\n            <span class=\"kp-nums\" id=\"kp-nums-desc\"><\/span>\n            <span class=\"kp-pill\" id=\"kp-pill-desc\"><\/span>\n          <\/div>\n        <\/div>\n        <span class=\"kp-sr-only\" id=\"kp-status-desc\" role=\"status\" aria-live=\"polite\"><\/span>\n      <\/div>\n\n      <div class=\"kp-field\">\n        <div class=\"kp-field-head\">\n          <label for=\"kp-in-url\">URL<\/label>\n          <span class=\"kp-label-hint\" id=\"kp-hint-url\">shown as breadcrumbs<\/span>\n        <\/div>\n        <input type=\"text\" id=\"kp-in-url\" spellcheck=\"false\" maxlength=\"500\" aria-describedby=\"kp-hint-url\" value=\"https:\/\/keypers.io\/en\/seo-audit\/\">\n      <\/div>\n    <\/div>\n\n    <!-- ---------------- preview ---------------- -->\n    <div class=\"kp-preview-col\">\n      <div class=\"kp-preview-head\">\n        <span class=\"kp-eyebrow\">Preview<\/span>\n        <span class=\"kp-eyebrow\" id=\"kp-device-label\">Desktop \u00b7&nbsp;Arial<\/span>\n      <\/div>\n\n      <div class=\"kp-bezel\">\n        <div class=\"kp-screen\" id=\"kp-screen\" data-device=\"desktop\">\n          <div class=\"kp-page\">\n            <!-- Decorative mock of a Google results page. The search field is a\n                 real input so a curious visitor can type in it, but nothing\n                 happens (it does not search). The logo and tabs are aria-hidden\n                 because they carry no information. -->\n            <div class=\"kp-gbar\">\n              <span class=\"kp-glogo\" aria-hidden=\"true\"><span>G<\/span><span>o<\/span><span>o<\/span><span>g<\/span><span>l<\/span><span>e<\/span><\/span>\n              <div class=\"kp-gsearch\">\n                <svg class=\"kp-gmag\" viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"7\" stroke=\"#9aa0a6\" stroke-width=\"2\" \/><line x1=\"16.5\" y1=\"16.5\" x2=\"21\" y2=\"21\" stroke=\"#9aa0a6\" stroke-width=\"2\" stroke-linecap=\"round\" \/><\/svg>\n                <input class=\"kp-ginput\" type=\"text\" tabindex=\"-1\" aria-label=\"Sample search box (does not search)\" placeholder=\"Search\u2026\">\n              <\/div>\n            <\/div>\n            <div class=\"kp-gtabs\" aria-hidden=\"true\">\n              <span class=\"kp-gtab kp-gtab--on\">All<\/span>\n              <span class=\"kp-gtab\">Images<\/span>\n              <span class=\"kp-gtab\">Videos<\/span>\n              <span class=\"kp-gtab\">News<\/span>\n              <span class=\"kp-gtab\">Maps<\/span>\n            <\/div>\n\n            <!-- The live snippet, marked as the highlighted top result. -->\n            <div class=\"kp-snippet kp-snippet--yours\">\n              <span class=\"kp-yours-tag\" aria-hidden=\"true\">Your preview<\/span>\n              <div class=\"kp-snippet-inner\">\n                <div class=\"kp-snippet-site\">\n                  <div class=\"kp-snippet-favicon\" id=\"kp-favicon\">K<\/div>\n                  <div class=\"kp-snippet-site-meta\">\n                    <div class=\"kp-snippet-site-name\" id=\"kp-site-name\">Keypers<\/div>\n                    <div class=\"kp-snippet-url\" id=\"kp-site-url\"><\/div>\n                  <\/div>\n                <\/div>\n                <div class=\"kp-snippet-title\" id=\"kp-out-title\"><\/div>\n                <div class=\"kp-snippet-desc\" id=\"kp-out-desc\"><\/div>\n              <\/div>\n            <\/div>\n\n            <!-- \"Still loading\" skeleton results, purely to frame yours as the\n                 top result. Decorative, and hidden when the layout stacks so\n                 they don't bury the inputs on a phone. -->\n            <div class=\"kp-skeletons\" aria-hidden=\"true\">\n              <div class=\"kp-skel\"><div class=\"kp-skel-site\"><span class=\"kp-skel-circle\"><\/span><span class=\"kp-skel-line\"><\/span><\/div><div class=\"kp-skel-title\"><\/div><div class=\"kp-skel-desc\"><\/div><\/div>\n            <\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"kp-cut-note\">\n        <span class=\"kp-cut-swatch\"><\/span>\n        <span>The hatched part won\u2019t appear in results because Google cuts it&nbsp;off.<\/span>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- ---------------- caveats ----------------\n       These are the things that make it obvious SEO people built this,\n       not a developer who found measureText() on Stack Overflow. -->\n  <section class=\"kp-caveats\" aria-label=\"Accuracy notes\">\n    <div class=\"kp-caveat\">\n      <h2>The limits are estimates, not gospel<\/h2>\n      <p>\n        Google publishes no exact figures and the sources disagree. For the\n        title, Screaming Frog historically measured 482&nbsp;px, while today\u2019s tools\n        work with more like 580\u2009\u2013\u2009600&nbsp;px. For the description the spread is even\n        wider: it actually renders around 930\u2009\u2013\u2009940&nbsp;px, Mangools lists a&nbsp;960&nbsp;px\n        limit, Screaming Frog up to 1005&nbsp;px. We take the more cautious numbers\n        from that&nbsp;range.&nbsp;<\/p>\n    <\/div>\n    <div class=\"kp-caveat\">\n      <h2>On mobile the limit is a&nbsp;budget, not a&nbsp;line width<\/h2>\n      <p>\n        A&nbsp;mobile title wraps onto two lines; Google doesn\u2019t cut it at the end\n        of the first. The 490&nbsp;px limit is therefore a&nbsp;total text budget, not\n        the width of a&nbsp;single line. That\u2019s why a&nbsp;467&nbsp;px title wraps in the\n        preview and still passes. Treat the mobile figures as indicative\n        rather than an exact cut-off.&nbsp;<\/p>\n    <\/div>\n    <div class=\"kp-caveat\">\n      <h2>Google may rewrite your title anyway<\/h2>\n      <p>\n        In roughly 60% of cases Google rewrites the title in the results\n        itself, based on your H1, link text or brand name. It may also add a\n        date to the description or bold words from the query, which eats a&nbsp;few\n        pixels. This tool tells you whether your draft fits, not whether Google\n        will use&nbsp;it.&nbsp;<\/p>\n    <\/div>\n    <div class=\"kp-caveat\">\n      <h2>We measure in the browser<\/h2>\n      <p>\n        The text is drawn onto a&nbsp;hidden canvas in Google\u2019s font (Arial 20&nbsp;px\n        for the desktop title, 18&nbsp;px for mobile, 14&nbsp;px for the description) and\n        its real width is measured. Nothing is sent anywhere; your data stays\n        in your browser. Font rendering differs by a&nbsp;few pixels between Windows,\n        Linux and&nbsp;macOS.&nbsp;<\/p>\n    <\/div>\n  <\/section>\n\n  <!-- ---------------- closing CTA ----------------\n       The one link from the tool into the funnel. The copy does not sell the\n       tool; it names what the tool cannot tell you, which is exactly what the\n       audit covers. -->\n  <aside class=\"kp-cta\">\n    <div class=\"kp-cta-text\">\n      <h2>This is one piece of the puzzle<\/h2>\n      <p>\n        Your title and description decide whether anyone clicks your result.\n        Whether you show up at all depends on the rest of the site. That is\n        what we go through in an SEO&nbsp;audit.&nbsp;<\/p>\n    <\/div>\n    <a class=\"kp-cta-btn\" href=\"https:\/\/keypers.io\/en\/seo-audit\/\">I&nbsp;want an SEO&nbsp;audit<\/a>\n  <\/aside>\n<\/div>\n<\/div>\n\n<script>\n(() => {\n  \"use strict\";\n\n  \/* ==================================================================\n     LIMITS \u2014 the one block worth arguing about.\n\n     Google publishes none of this; every number below is community-derived\n     and shifts whenever Google reflows the SERP. IMPORTANT: a pixel limit is\n     only meaningful paired with the font size it was measured at. These fonts\n     (20px desktop title \/ 18px mobile title \/ 14px desc) are the tool's own\n     measurement fonts, so any limit you set here must be derived by measuring\n     a REAL truncated SERP string in these same sizes \u2014 not borrowed from a\n     tool that measures at 18px\/13px (e.g. Screaming Frog's \"561px\" title is an\n     18px number \u2248 ~620px at 20px). The mobile title especially is a two-line\n     text budget, not a single-line cut, and is the least certain value.\n\n     `warn` is where we start nagging, `limit` is where Google cuts.\n     Tune freely \u2014 the rest of the code reads these and nothing else.\n     ================================================================== *\/\n  const LIMITS = {\n    desktop: {\n      title: { font: \"20px Arial, 'Liberation Sans', sans-serif\", warn: 560, limit: 580 },\n      \/\/ Desc: raised per Michal's real-render testing (descriptions show at\n      \/\/ 930\u2013940 px) and the tools he cited (Mangools cuts at 960, Screaming\n      \/\/ Frog lists 1005). limit=960 (Mangools) is the conservative cut point;\n      \/\/ warn=920 is the old estimate, now the \"you're in contested territory\"\n      \/\/ line. Bump to 1005 if you want to match Screaming Frog.\n      desc:  { font: \"14px Arial, 'Liberation Sans', sans-serif\", warn: 920, limit: 960 }\n    },\n    mobile: {\n      title: { font: \"18px Arial, 'Liberation Sans', sans-serif\", warn: 460, limit: 490 },\n      desc:  { font: \"14px Arial, 'Liberation Sans', sans-serif\", warn: 640, limit: 680 }\n    }\n  };\n\n  const ELLIPSIS = \"\u2026\";\n\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n\n  \/** Width of `text` in CSS pixels when rendered in `font`. *\/\n  const widthOf = (text, font) => {\n    ctx.font = font;\n    return ctx.measureText(text).width;\n  };\n\n  \/** Grapheme-aware segmentation, so emoji count as one and Czech diacritics\n      don't get split into base + combining mark. Used for both counting and\n      truncation, so the two never disagree. *\/\n  const segmenter = typeof Intl !== \"undefined\" && Intl.Segmenter\n    ? new Intl.Segmenter(\"en\", { granularity: \"grapheme\" })\n    : null;\n  const toGraphemes = (text) =>\n    segmenter ? [...segmenter.segment(text)].map((s) => s.segment) : [...text];\n  const countChars = (text) => toGraphemes(text).length;\n\n  \/**\n   * Split `text` at the point Google would truncate it.\n   * Returns { kept, cut } \u2014 `cut` is \"\" when the whole string fits.\n   * Google backs off to a word boundary and appends an ellipsis.\n   * Works over graphemes (never splits an emoji \/ combining mark) and backs\n   * off on any whitespace, including the U+00A0 non-breaking spaces Czech\n   * puts after one-letter prepositions (v, s, k, z, a, i, o, u).\n   *\/\n  function splitAtTruncation(text, font, limit) {\n    if (!text) return { kept: \"\", cut: \"\" };\n    if (widthOf(text, font) <= limit) return { kept: text, cut: \"\" };\n\n    const budget = limit - widthOf(ELLIPSIS, font);\n    const units = toGraphemes(text);\n\n    \/\/ Binary search for the longest grapheme prefix that fits with the ellipsis.\n    let lo = 0, hi = units.length;\n    while (lo < hi) {\n      const mid = Math.ceil((lo + hi) \/ 2);\n      if (widthOf(units.slice(0, mid).join(\"\"), font) <= budget) lo = mid;\n      else hi = mid - 1;\n    }\n\n    let kept = units.slice(0, lo).join(\"\");\n    \/\/ Back off to the last whitespace, but never to nothing.\n    const m = kept.match(\/[\\s\u00a0][^\\s\u00a0]*$\/);\n    if (m && m.index > 0) kept = kept.slice(0, m.index);\n\n    return { kept, cut: text.slice(kept.length) };\n  }\n\n  const state = (w, spec) =>\n    w > spec.limit ? \"over\" : w >= spec.warn ? \"warn\" : \"ok\";\n\n  const LABEL = { ok: \"Fits\", warn: \"Borderline\", over: \"Cut off\" };\n\n  const el = (id) => document.getElementById(id);\n\n  const ui = {\n    title: {\n      input: el(\"kp-in-title\"), out: el(\"kp-out-title\"),\n      fill: el(\"kp-fill-title\"), tick: el(\"kp-tick-title\"),\n      nums: el(\"kp-nums-title\"), pill: el(\"kp-pill-title\"),\n      status: el(\"kp-status-title\"), name: \"Title\", prev: null,\n      empty: \"No title\"\n    },\n    desc: {\n      input: el(\"kp-in-desc\"), out: el(\"kp-out-desc\"),\n      fill: el(\"kp-fill-desc\"), tick: el(\"kp-tick-desc\"),\n      nums: el(\"kp-nums-desc\"), pill: el(\"kp-pill-desc\"),\n      status: el(\"kp-status-desc\"), name: \"Description\", prev: null,\n      empty: \"Without a description, Google generates a snippet from the page itself.\"\n    }\n  };\n\n  let device = \"desktop\";\n\n  \/** Render one measured field: preview text, ruler, readout, pill. *\/\n  function renderField(key) {\n    const spec = LIMITS[device][key];\n    const node = ui[key];\n    const text = node.input.value.trim();\n\n    const w = text ? widthOf(text, spec.font) : 0;\n    const { kept, cut } = splitAtTruncation(text, spec.font, spec.limit);\n    const st = state(w, spec);\n\n    \/\/ Preview \u2014 truncated tail stays visible but marked as lost. The hatch\n    \/\/ texture (not colour alone) carries \"this is cut\" for sighted users; the\n    \/\/ sr-only wrappers carry it for screen readers.\n    node.out.textContent = \"\";\n    if (!text) {\n      const span = document.createElement(\"span\");\n      span.className = \"kp-snippet-empty\";\n      span.textContent = node.empty;\n      node.out.append(span);\n    } else {\n      node.out.append(document.createTextNode(kept));\n      if (cut) {\n        node.out.append(document.createTextNode(ELLIPSIS));\n        const srOpen = document.createElement(\"span\");\n        srOpen.className = \"kp-sr-only\";\n        srOpen.textContent = \" (cut: \";\n        node.out.append(srOpen);\n        const ghost = document.createElement(\"span\");\n        ghost.className = \"kp-ghost\";\n        ghost.textContent = cut;\n        node.out.append(ghost);\n        const srClose = document.createElement(\"span\");\n        srClose.className = \"kp-sr-only\";\n        srClose.textContent = \")\";\n        node.out.append(srClose);\n      }\n    }\n\n    \/\/ Ruler \u2014 the track represents the limit exactly.\n    node.fill.style.width = Math.min(w \/ spec.limit, 1) * 100 + \"%\";\n    node.fill.dataset.state = st;\n    node.tick.style.left = (spec.warn \/ spec.limit) * 100 + \"%\";\n\n    \/\/ Readout, assembled from nodes rather than parsed from a string.\n    \/\/ Only numbers are interpolated here, so innerHTML would be safe *today* \u2014\n    \/\/ but this page runs on keypers.io, where script in the page shares an\n    \/\/ origin with a logged-in WordPress session. Keeping the file free of\n    \/\/ HTML-parsing sinks makes that a structural property instead of something\n    \/\/ a later edit can quietly undo by dropping a string in.\n    const px = Math.round(w);\n    const over = px - spec.limit;\n\n    const strong = (v) => {\n      const b = document.createElement(\"b\");\n      b.textContent = v;\n      return b;\n    };\n    const sep = () => {\n      const s = document.createElement(\"span\");\n      s.className = \"kp-sep\";\n      s.textContent = \" \u00b7 \";\n      return s;\n    };\n\n    node.nums.textContent = \"\";\n    node.nums.append(strong(px), `\/${spec.limit} px`, sep(), strong(countChars(text)), \" chars\");\n    if (over > 0) node.nums.append(sep(), `+${over} px over`);\n\n    node.pill.dataset.state = st;\n    node.pill.textContent = LABEL[st];\n\n    \/\/ Screen-reader announcement, only when the state actually changes, so\n    \/\/ typing doesn't flood the live region on every keystroke.\n    if (node.prev !== st) {\n      node.status.textContent = `${node.name}: ${px} of ${spec.limit} pixels, ${LABEL[st]}.`;\n      node.prev = st;\n    }\n  }\n\n  \/** Google shows brand + breadcrumbs now, not a raw URL. *\/\n  function renderUrl() {\n    const raw = el(\"kp-in-url\").value.trim();\n    let host = \"\", crumbs = [];\n    \/\/ Note: this whole function is deliberately free of any literal \"\/\/\" (and of\n    \/\/ any regex containing two adjacent slashes). An over-eager inline-JS\n    \/\/ minifier (e.g. Breeze) that isn't string\/regex-aware would treat \"\/\/\" as a\n    \/\/ line comment and truncate the line, killing the entire script. SLASH lets\n    \/\/ us build \"\/\/\" at runtime without writing it in the source.\n    const SLASH = \"\/\";\n    let u = null;\n    try { u = new URL(raw); } catch (e) { u = null; }                 \/\/ already has a scheme\n    if (!u) { try { u = new URL(\"https:\" + SLASH + SLASH + raw); } catch (e) { u = null; } }\n    if (u) {\n      host = u.hostname.replace(\/^www\\.\/, \"\");\n      crumbs = u.pathname.split(SLASH).filter(Boolean).map((s) => {\n        try { return decodeURIComponent(s); } catch (e) { return s; }\n      });\n    } else {\n      host = raw.replace(\/^[a-z][\\w+.-]*:\/i, \"\").replace(\/^\\\/+\/, \"\")\n                .replace(\/^www\\.\/, \"\").split(SLASH)[0];\n    }\n\n    const brand = host ? host.split(\".\")[0] : \"\";\n    el(\"kp-site-name\").textContent = brand\n      ? brand.charAt(0).toUpperCase() + brand.slice(1)\n      : \"Site name\";\n    el(\"kp-favicon\").textContent = (brand.charAt(0) || \"?\").toUpperCase();\n\n    \/\/ Google dropped the path breadcrumb on mobile results in Jan 2025 \u2014\n    \/\/ mobile now shows site name + domain only. Desktop keeps the crumbs.\n    const crumbsToShow = device === \"mobile\" ? [] : crumbs;\n    el(\"kp-site-url\").textContent = host\n      ? [host, ...crumbsToShow].join(\" \u203a \")\n      : \"example.com\";\n  }\n\n  function renderAll() {\n    renderField(\"title\");\n    renderField(\"desc\");\n    renderUrl();\n    el(\"kp-screen\").dataset.device = device;\n    el(\"kp-device-label\").textContent =\n      (device === \"desktop\" ? \"Desktop\" : \"Mobile\") + \" \u00b7 Arial\";\n  }\n\n  \/\/ ---- wiring ----\n  [ui.title.input, ui.desc.input].forEach((n) =>\n    n.addEventListener(\"input\", () => renderField(n === ui.title.input ? \"title\" : \"desc\"))\n  );\n  el(\"kp-in-url\").addEventListener(\"input\", renderUrl);\n\n  const btnD = el(\"kp-btn-desktop\"), btnM = el(\"kp-btn-mobile\");\n  const setDevice = (d) => {\n    device = d;\n    btnD.setAttribute(\"aria-pressed\", String(d === \"desktop\"));\n    btnM.setAttribute(\"aria-pressed\", String(d === \"mobile\"));\n    renderAll();\n  };\n  btnD.addEventListener(\"click\", () => setDevice(\"desktop\"));\n  btnM.addEventListener(\"click\", () => setDevice(\"mobile\"));\n\n  \/\/ Measure once now (Arial is a local font, available immediately). The\n  \/\/ fonts.ready re-render only matters for the Booton UI chrome swapping in;\n  \/\/ the Arial-measured numbers don't change. It never rejects, so a missing\n  \/\/ Booton can't hang it.\n  renderAll();\n  if (document.fonts && document.fonts.ready) document.fonts.ready.then(renderAll);\n})();\n<\/script>\n\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t<\/div>\n","protected":false},"excerpt":{"rendered":"<p>SERP simulator Type a&nbsp;title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut&nbsp;off.&nbsp; Desktop Mobile Page title &lt;title&gt; Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"_acf":"","inline_featured_image":false,"footnotes":""},"class_list":["post-2006","page","type-page","status-publish","hentry"],"acf":[],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO Pro 4.9.10 - aioseo.com -->\n\t<meta name=\"description\" content=\"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"google-site-verification\" content=\"6FT1oxYFaWwRgVdupZ10HXBoM2OLHJKQuxXIOhRIZnw\" \/>\n\t<link rel=\"canonical\" href=\"https:\/\/keypers.io\/en\/serp-simulator\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO Pro (AIOSEO) 4.9.10\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"Keypers\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"SERP Simulator | Keypers\" \/>\n\t\t<meta property=\"og:description\" content=\"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/keypers.io\/en\/serp-simulator\/\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/keypers.io\/wp-content\/uploads\/navrh-bez-nazvu-2.png\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/keypers.io\/wp-content\/uploads\/navrh-bez-nazvu-2.png\" \/>\n\t\t<meta property=\"og:image:width\" content=\"320\" \/>\n\t\t<meta property=\"og:image:height\" content=\"320\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-07-19T09:24:59+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-07-21T09:38:28+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary\" \/>\n\t\t<meta name=\"twitter:title\" content=\"SERP Simulator | Keypers\" \/>\n\t\t<meta name=\"twitter:description\" content=\"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL\" \/>\n\t\t<meta name=\"twitter:image\" content=\"https:\/\/keypers.io\/wp-content\/uploads\/navrh-bez-nazvu-2.png\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/serp-simulator\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/keypers.io\\\/en\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/serp-simulator\\\/#listItem\",\"name\":\"SERP Simulator\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/serp-simulator\\\/#listItem\",\"position\":2,\"name\":\"SERP Simulator\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/#listItem\",\"name\":\"Home\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/#organization\",\"name\":\"Keypers\",\"url\":\"https:\\\/\\\/keypers.io\\\/en\\\/\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/serp-simulator\\\/#webpage\",\"url\":\"https:\\\/\\\/keypers.io\\\/en\\\/serp-simulator\\\/\",\"name\":\"SERP Simulator | Keypers\",\"description\":\"SERP simulator Type a title and description and instantly see how they\\u2019ll appear in Google\\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/serp-simulator\\\/#breadcrumblist\"},\"datePublished\":\"2026-07-19T11:24:59+02:00\",\"dateModified\":\"2026-07-21T11:38:28+02:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/#website\",\"url\":\"https:\\\/\\\/keypers.io\\\/en\\\/\",\"name\":\"Keypers\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/keypers.io\\\/en\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO Pro -->\r\n\t\t<title>SERP Simulator | Keypers<\/title>\n\n","aioseo_head_json":{"title":"SERP Simulator | Keypers","description":"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL","canonical_url":"https:\/\/keypers.io\/en\/serp-simulator\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"google-site-verification":"6FT1oxYFaWwRgVdupZ10HXBoM2OLHJKQuxXIOhRIZnw","miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BreadcrumbList","@id":"https:\/\/keypers.io\/en\/serp-simulator\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/keypers.io\/en\/#listItem","position":1,"name":"Home","item":"https:\/\/keypers.io\/en\/","nextItem":{"@type":"ListItem","@id":"https:\/\/keypers.io\/en\/serp-simulator\/#listItem","name":"SERP Simulator"}},{"@type":"ListItem","@id":"https:\/\/keypers.io\/en\/serp-simulator\/#listItem","position":2,"name":"SERP Simulator","previousItem":{"@type":"ListItem","@id":"https:\/\/keypers.io\/en\/#listItem","name":"Home"}}]},{"@type":"Organization","@id":"https:\/\/keypers.io\/en\/#organization","name":"Keypers","url":"https:\/\/keypers.io\/en\/"},{"@type":"WebPage","@id":"https:\/\/keypers.io\/en\/serp-simulator\/#webpage","url":"https:\/\/keypers.io\/en\/serp-simulator\/","name":"SERP Simulator | Keypers","description":"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/keypers.io\/en\/#website"},"breadcrumb":{"@id":"https:\/\/keypers.io\/en\/serp-simulator\/#breadcrumblist"},"datePublished":"2026-07-19T11:24:59+02:00","dateModified":"2026-07-21T11:38:28+02:00"},{"@type":"WebSite","@id":"https:\/\/keypers.io\/en\/#website","url":"https:\/\/keypers.io\/en\/","name":"Keypers","inLanguage":"en-US","publisher":{"@id":"https:\/\/keypers.io\/en\/#organization"}}]},"og:locale":"en_US","og:site_name":"Keypers","og:type":"article","og:title":"SERP Simulator | Keypers","og:description":"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL","og:url":"https:\/\/keypers.io\/en\/serp-simulator\/","og:image":"https:\/\/keypers.io\/wp-content\/uploads\/navrh-bez-nazvu-2.png","og:image:secure_url":"https:\/\/keypers.io\/wp-content\/uploads\/navrh-bez-nazvu-2.png","og:image:width":320,"og:image:height":320,"article:published_time":"2026-07-19T09:24:59+00:00","article:modified_time":"2026-07-21T09:38:28+00:00","twitter:card":"summary","twitter:title":"SERP Simulator | Keypers","twitter:description":"SERP simulator Type a title and description and instantly see how they\u2019ll appear in Google\u2019s search results, and where they get cut off. Desktop Mobile Page title Description meta description We find why your online store earns less from organic search than it could. Concrete briefs for developers, not vague advice. 11 years, 350 clients. URL","twitter:image":"https:\/\/keypers.io\/wp-content\/uploads\/navrh-bez-nazvu-2.png"},"aioseo_meta_data":{"post_id":"2006","title":null,"description":null,"keywords":null,"keyphrases":{"focus":{"keyphrase":"","score":0,"analysis":{"keyphraseInTitle":{"score":0,"maxScore":9,"error":1}}},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"WebPage","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":null,"frequency":"default","local_seo":null,"seo_analyzer_scan_date":"2026-07-21 09:40:06","breadcrumb_settings":null,"limit_modified_date":false,"open_ai":null,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-07-19 09:55:18","updated":"2026-07-21 09:40:06"},"_links":{"self":[{"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/pages\/2006","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/comments?post=2006"}],"version-history":[{"count":5,"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/pages\/2006\/revisions"}],"predecessor-version":[{"id":2016,"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/pages\/2006\/revisions\/2016"}],"wp:attachment":[{"href":"https:\/\/keypers.io\/en\/wp-json\/wp\/v2\/media?parent=2006"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}