{"id":32492,"date":"2025-12-04T12:17:00","date_gmt":"2025-12-04T12:17:00","guid":{"rendered":"https:\/\/obiajor.ubi.pt\/?page_id=32492"},"modified":"2026-10-14T16:01:00","modified_gmt":"2026-10-14T16:01:00","slug":"podcast","status":"publish","type":"page","link":"http:\/\/obiajor.ubi.pt\/en\/divulgacao\/podcast\/","title":{"rendered":"PodCast"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"32492\" class=\"elementor elementor-32492\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-bcaa8fb e-flex e-con-boxed e-con e-parent\" data-id=\"bcaa8fb\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2678f7d jltma-glass-effect-no elementor-widget elementor-widget-html\" data-id=\"2678f7d\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<div class=\"obj-player\">\n  <div class=\"obj-row\">\n    <img decoding=\"async\" class=\"obj-cover\" src=\"\/wp-content\/uploads\/2026\/10\/Spotify-1-scaled.png\" alt=\"\u00d3bia Jor\" onerror=\"this.style.display='none'\">\n    <div class=\"obj-main\">\n      <div class=\"obj-title\"><\/div>\n      <div class=\"obj-controls\">\n        <button class=\"obj-play\" type=\"button\" aria-label=\"Play\">\n          <svg viewBox=\"0 0 24 24\" width=\"22\" height=\"22\" aria-hidden=\"true\"><path class=\"obj-icon\" d=\"M8 5v14l11-7z\"\/><\/svg>\n        <\/button>\n        <input class=\"obj-seek\" type=\"range\" min=\"0\" max=\"1000\" value=\"0\" step=\"1\" aria-label=\"Seek\">\n        <span class=\"obj-time\">0:00<\/span>\n      <\/div>\n    <\/div>\n  <\/div>\n  <div class=\"obj-captions\" aria-live=\"off\"><div class=\"obj-track\"><\/div><\/div>\n  <audio preload=\"metadata\"><\/audio>\n  <!-- One <li> per episode. The ?ep= value is what appears in the URL; data-cover is optional (default: the image above). -->\n  <h2 class=\"obj-list-title\">Epis\u00f3dios<\/h2>\n  <ul class=\"obj-list\">\n    <li><a href=\"?ep=1\" data-audio=\"\/wp-content\/uploads\/2026\/10\/ep1.mp3\" data-vtt=\"\/wp-content\/uploads\/2026\/10\/ep1.vtt\">1. O que sabes sobre o OBIAJOR?<\/a><\/li>\n    <!-- <li><a href=\"?ep=2\" data-audio=\"\/wp-content\/uploads\/2026\/10\/ep2.mp3\" data-vtt=\"\/wp-content\/uploads\/2026\/10\/ep2.vtt\">2. ...<\/a><\/li> -->\n  <\/ul>\n<\/div>\n<style>\n\/* Colours follow the theme (currentColor), so it works on light and dark sites. *\/\n.obj-player { max-width: 720px; margin: 2em auto; color: inherit; font-family: inherit; box-sizing: border-box; }\n.obj-player *, .obj-player *::before, .obj-player *::after { box-sizing: border-box; }\n.obj-player .obj-row { display: flex; align-items: center; gap: 24px; margin: 0; }\n.obj-player .obj-cover { display: block; width: 160px; height: 160px; max-width: none; object-fit: cover; border-radius: 12px; flex: none; margin: 0; box-shadow: none; }\n.obj-player .obj-main { flex: 1; min-width: 0; }\n.obj-player .obj-title { margin: 0 0 12px; font-size: 1.15em; font-weight: 600; line-height: 1.3; }\n.obj-player .obj-controls { display: flex; align-items: center; gap: 14px; min-width: 0; }\n.obj-player .obj-play { flex: none !important; width: 48px !important; height: 48px !important; min-width: 0 !important; min-height: 0 !important; border-radius: 50% !important; border: 1.5px solid currentColor !important; padding: 0 !important; margin: 0 !important; background: transparent !important; color: inherit !important; cursor: pointer; display: grid !important; place-items: center; box-shadow: none !important; line-height: 1 !important; }\n.obj-player .obj-play:hover { background: color-mix(in srgb, currentColor 10%, transparent) !important; }\n.obj-player .obj-play svg { width: 22px !important; height: 22px !important; fill: currentColor !important; display: block; }\n.obj-player .obj-seek { flex: 1; min-width: 0; width: auto; height: 4px; margin: 0; padding: 0; border: 0; border-radius: 2px; cursor: pointer; appearance: none; -webkit-appearance: none; background: linear-gradient(to right, currentColor var(--p, 0%), color-mix(in srgb, currentColor 18%, transparent) var(--p, 0%)); }\n.obj-player .obj-seek::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: currentColor; border: 0; }\n.obj-player .obj-seek::-moz-range-thumb { width: 12px; height: 12px; border: 0; border-radius: 50%; background: currentColor; }\n.obj-player .obj-time { flex: none; font-size: .85em; font-variant-numeric: tabular-nums; opacity: .55; white-space: nowrap; }\n.obj-player .obj-captions { position: relative; margin: 28px 0 0; height: 11em; overflow: hidden; text-align: center; -webkit-mask-image: linear-gradient(transparent, #000 28%, #000 72%, transparent); mask-image: linear-gradient(transparent, #000 28%, #000 72%, transparent); }\n\/* Narrower than the player: shorter lines read better, and taller cues make the scroll easier to see. *\/\n.obj-player .obj-track { max-width: 26em; margin: 0 auto; will-change: transform; }\n.obj-player .obj-line { margin: 0 0 .9em; padding: 0; font-size: 1.15em; line-height: 1.5; opacity: .3; cursor: pointer; transition: opacity .8s ease; }\n\/* Light up fast so LEAD is visible; the previous line still fades out slowly. *\/\n.obj-player .obj-line.obj-on { opacity: 1; cursor: default; transition-duration: .15s; }\n.obj-player .obj-who { display: block; font-size: .6em; letter-spacing: .08em; text-transform: uppercase; opacity: .6; }\n\/* Same look as the site's menu (Elementor nav: Raleway 700, uppercase). *\/\n.obj-player .obj-list-title { margin: 40px 0 12px; padding: 0; font-family: \"Raleway\", sans-serif; font-size: 1em; font-weight: 700; line-height: 1.3; letter-spacing: normal; text-transform: uppercase; color: inherit; }\n.obj-player .obj-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent); }\n.obj-player .obj-list li { margin: 0; padding: 0; border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent); }\n.obj-player .obj-list a { display: block; padding: .8em 0; color: inherit; text-decoration: none; opacity: .6; }\n.obj-player .obj-list a:hover, .obj-player .obj-list a[aria-current] { opacity: 1; }\n.obj-player .obj-list a[aria-current] { font-weight: 600; }\n@media (prefers-reduced-motion: reduce) { .obj-player .obj-line { transition: none; } }\n@media (max-width: 520px) {\n  .obj-player .obj-row { flex-direction: column; align-items: stretch; }\n  .obj-player .obj-cover { width: 100%; height: auto; aspect-ratio: 1; }\n}\n<\/style>\n<script>\n(function () {\n  \/\/ \"00:01:02.500\" -> seconds\n  const secs = t => t.split(\":\").reduce((a, v) => a * 60 + parseFloat(v), 0);\n  const fmt = s => Math.floor(s \/ 60) + \":\" + String(Math.floor(s % 60)).padStart(2, \"0\");\n  \/\/ Show each caption this many seconds before its timestamp: reading a line just ahead of\n  \/\/ hearing it feels in sync, while showing it exactly on time feels late.\n  const LEAD = 0.5;\n  \/\/ A leading \"Name: \" becomes a small speaker label (names are added by hand in the .vtt).\n  const SPEAKER = \/^([A-Z\u00c0-\u00dd][\\p{L}\\d_.-]*(?: [\\p{L}\\d_.-]+){0,2}):\\s+\/u;\n  function parse(vtt) {\n    return vtt.replace(\/\\r\/g, \"\").split(\/\\n\\n+\/).flatMap(block => {\n      const lines = block.split(\"\\n\");\n      const i = lines.findIndex(l => l.includes(\"-->\"));\n      if (i < 0) return [];\n      const [a, b] = lines[i].split(\"-->\").map(s => secs(s.trim().split(\" \")[0]));\n      const text = lines.slice(i + 1).join(\" \");\n      const m = text.match(SPEAKER);\n      return [{ start: a, end: b, who: m && m[1], text: m ? text.slice(m[0].length) : text }];\n    });\n  }\n  document.querySelectorAll(\".obj-player\").forEach(root => {\n    const $ = s => root.querySelector(s);\n    const audio = $(\"audio\"), play = $(\".obj-play\"), seek = $(\".obj-seek\"), time = $(\".obj-time\");\n    const caps = $(\".obj-captions\"), track = $(\".obj-track\"), cover = $(\".obj-cover\"), title = $(\".obj-title\");\n    const icon = play.querySelector(\".obj-icon\");\n    const eps = [...root.querySelectorAll(\".obj-list a\")];\n    const epId = a => new URL(a.href).searchParams.get(\"ep\");\n    const defaultCover = cover.src;\n    const still = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let cues = [], lines = [], pos = [], slope = [], half = 0, cur = -2, ep = null, y = null;\n    \/\/ Show the episode named by ?ep= (the first one if it is missing or unknown).\n    function fromUrl() {\n      const want = new URLSearchParams(location.search).get(\"ep\");\n      load(eps.find(a => epId(a) === want) || eps[0]);\n    }\n    function load(a) {\n      if (!a || a === ep) return;\n      ep = a;\n      eps.forEach(x => x === a ? x.setAttribute(\"aria-current\", \"true\") : x.removeAttribute(\"aria-current\"));\n      title.textContent = a.textContent;\n      cover.style.display = \"\";\n      cover.src = a.dataset.cover || defaultCover;\n      audio.src = a.dataset.audio;\n      cues = []; lines = []; pos = []; cur = -2; y = null; track.replaceChildren(); tick();\n      fetch(a.dataset.vtt).then(r => r.text()).then(t => {\n        if (ep !== a) return; \/\/ another episode was picked while this one loaded\n        if (!t.trimStart().startsWith(\"WEBVTT\")) throw new Error(\"not a .vtt file: \" + a.dataset.vtt);\n        cues = parse(t); build();\n      }).catch(e => console.error(\"obj-player:\", e.message));\n    }\n    eps.forEach(a => a.onclick = e => {\n      if (e.metaKey || e.ctrlKey || e.shiftKey) return; \/\/ keep \"open in new tab\"\n      e.preventDefault();\n      if (a !== ep) {\n        \/\/ Build from the current URL so other query args (e.g. WordPress preview) survive.\n        const u = new URL(location.href);\n        u.searchParams.set(\"ep\", epId(a));\n        history.pushState(null, \"\", u);\n        load(a);\n      }\n      audio.play().catch(() => {}); \/\/ rejects harmlessly if the source changes mid-load\n    });\n    addEventListener(\"popstate\", fromUrl);\n    function build() {\n      lines = cues.map(c => {\n        const p = document.createElement(\"p\");\n        p.className = \"obj-line\";\n        if (c.who) {\n          const w = document.createElement(\"span\");\n          w.className = \"obj-who\";\n          w.textContent = c.who;\n          p.append(w);\n        }\n        p.append(c.text);\n        p.onclick = () => { audio.currentTime = c.start; tick(); };\n        track.append(p);\n        return p;\n      });\n      measure();\n    }\n    function render() {\n      \/\/ Last cue that has started; -1 before the first one.\n      let i = -1;\n      for (let k = 0; k < cues.length && cues[k].start <= audio.currentTime + LEAD; k++) i = k;\n      if (i === cur) return;\n      if (lines[cur]) lines[cur].classList.remove(\"obj-on\");\n      cur = i;\n      if (lines[i]) lines[i].classList.add(\"obj-on\");\n    }\n    \/\/ Layout is read once here, not every frame. Each line's centre, and the scroll speed at each\n    \/\/ cue start: the harmonic mean of the speeds either side, so the motion never jolts when the\n    \/\/ cue changes and never runs backwards (monotone cubic interpolation).\n    function measure() {\n      half = caps.clientHeight \/ 2;\n      pos = lines.map(el => el.offsetTop + el.offsetHeight \/ 2);\n      const d = pos.slice(1).map((p, k) => (p - pos[k]) \/ Math.max(cues[k + 1].start - cues[k].start, .05));\n      slope = pos.map((_, k) => k === 0 ? d[0] : k === d.length ? d[k - 1] : 2 * d[k - 1] * d[k] \/ (d[k - 1] + d[k]));\n    }\n    new ResizeObserver(measure).observe(track); \/\/ also catches web fonts finishing loading\n    \/\/ Point of the track to show in the middle of the box at time t: each line is centred when its\n    \/\/ cue starts and glides on towards the next, so the text is always moving while audio plays.\n    function at(t) {\n      const k = cur;\n      if (k < 0 || still) return pos[Math.max(k, 0)];\n      if (k >= pos.length - 1) return pos[pos.length - 1];\n      const h = Math.max(cues[k + 1].start - cues[k].start, .05), s = Math.min((t - cues[k].start) \/ h, 1);\n      return (2 * s ** 3 - 3 * s ** 2 + 1) * pos[k] + (s ** 3 - 2 * s ** 2 + s) * h * slope[k]\n           + (3 * s ** 2 - 2 * s ** 3) * pos[k + 1] + (s ** 3 - s ** 2) * h * slope[k + 1];\n    }\n    \/\/ Some browsers advance currentTime in coarse steps; fill the gaps from the frame clock.\n    let seen = -1, seenAt = 0, last = 0;\n    function clock(now) {\n      const t = audio.currentTime + LEAD;\n      if (t !== seen) { seen = t; seenAt = now; }\n      return audio.paused ? t : t + Math.min((now - seenAt) \/ 1000, .3) * audio.playbackRate;\n    }\n    \/\/ timeupdate fires only ~4 times a second, too coarse for smooth motion, so follow the audio every frame.\n    (function frame(now) {\n      requestAnimationFrame(frame);\n      render();\n      const dt = now - last;\n      last = now;\n      if (!pos.length) return;\n      const goal = half - at(clock(now));\n      \/\/ Glide to the goal (absorbs seeks and clock corrections), but snap on big jumps\n      \/\/ (far seek, new episode) instead of flying past the text.\n      y = y === null || still || Math.abs(goal - y) > 2 * half ? goal : y + (goal - y) * (1 - Math.exp(-dt \/ 100));\n      track.style.transform = \"translate3d(0, \" + y + \"px, 0)\";\n    })(performance.now());\n    function tick() {\n      const d = audio.duration || 0;\n      seek.value = d ? (audio.currentTime \/ d) * 1000 : 0;\n      seek.style.setProperty(\"--p\", seek.value \/ 10 + \"%\");\n      time.textContent = fmt(audio.currentTime) + (d ? \" \/ \" + fmt(d) : \"\");\n    }\n    \/\/ \"emptied\" fires when the source changes, which stops playback without a \"pause\" event.\n    function sync() {\n      icon.setAttribute(\"d\", audio.paused ? \"M8 5v14l11-7z\" : \"M6 5h4v14H6zM14 5h4v14h-4z\");\n      play.ariaLabel = audio.paused ? \"Play\" : \"Pause\";\n    }\n    play.onclick = () => (audio.paused ? audio.play() : audio.pause());\n    audio.onplay = audio.onpause = audio.onemptied = sync;\n    audio.ontimeupdate = audio.onloadedmetadata = audio.onseeked = tick;\n    seek.oninput = () => { audio.currentTime = (seek.value \/ 1000) * (audio.duration || 0); };\n    fromUrl();\n  });\n})();\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>0:00 Epis\u00f3dios 1. O que sabes sobre o OBIAJOR?<\/p>","protected":false},"author":1,"featured_media":0,"parent":32490,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-32492","page","type-page","status-publish","hentry"],"acf":[],"_links":{"self":[{"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/pages\/32492","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/comments?post=32492"}],"version-history":[{"count":7,"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/pages\/32492\/revisions"}],"predecessor-version":[{"id":32760,"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/pages\/32492\/revisions\/32760"}],"up":[{"embeddable":true,"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/pages\/32490"}],"wp:attachment":[{"href":"http:\/\/obiajor.ubi.pt\/en\/wp-json\/wp\/v2\/media?parent=32492"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}