diff --git a/lib/anubis.php b/lib/anubis.php
index ec85ec7..7420343 100644
--- a/lib/anubis.php
+++ b/lib/anubis.php
@@ -45,15 +45,15 @@ class anubis{
if(
!isset($script["challenge"]) ||
+ !is_string($script["challenge"]["randomData"]) ||
!isset($script["rules"]["difficulty"]) ||
- !is_int($script["rules"]["difficulty"]) ||
- !is_string($script["challenge"])
+ !is_int($script["rules"]["difficulty"])
){
throw new Exception("Found invalid challenge data");
}
- return $this->rape($script["challenge"], $script["rules"]["difficulty"]);
+ return $this->rape($script["challenge"]["id"], $script["challenge"]["randomData"], $script["rules"]["difficulty"]);
}
private function is_valid_hash($hash, $difficulty){
@@ -74,9 +74,10 @@ class anubis{
return true;
}
- public function rape($data, $difficulty = 5){
+ public function rape($id, $data, $difficulty = 5){
$nonce = 0;
+ $microtime = microtime(true);
while(true){
@@ -87,10 +88,12 @@ class anubis{
$hash_hex = bin2hex($hash_binary);
return [
+ "id" => $id,
"response" => $hash_hex,
//"data" => $data,
//"difficulty" => $difficulty,
- "nonce" => $nonce
+ "nonce" => $nonce,
+ "elapsedTime" => round((microtime(true) - $microtime) * 1000)
];
}
diff --git a/lib/frontend.php b/lib/frontend.php
index 604c628..4998adf 100644
--- a/lib/frontend.php
+++ b/lib/frontend.php
@@ -644,7 +644,6 @@ class frontend{
"google_cse" => "Google CSE",
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
- "qwant" => "Qwant",
"yep" => "Yep",
"mwmbl" => "Mwmbl",
"mojeek" => "Mojeek",
@@ -671,7 +670,6 @@ class frontend{
"google_cse" => "Google CSE",
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
- "qwant" => "Qwant",
"naver" => "Naver",
"baidu" => "Baidu",
"solofield" => "Solofield",
@@ -705,7 +703,6 @@ class frontend{
"google" => "Google",
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
- "qwant" => "Qwant",
"naver" => "Naver",
"baidu" => "Baidu",
"coccoc" => "Cốc Cốc",
@@ -724,7 +721,6 @@ class frontend{
"google" => "Google",
"yahoo_japan" => "Yahoo! JAPAN",
"startpage" => "Startpage",
- "qwant" => "Qwant",
//"mojeek" => "Mojeek",
"baidu" => "Baidu"
]
diff --git a/scraper/brave.php b/scraper/brave.php
index 5573133..2efaefb 100644
--- a/scraper/brave.php
+++ b/scraper/brave.php
@@ -383,6 +383,8 @@ class brave{
// load html
$this->fuckhtml->load($html);
+ $this->detect_captcha($html);
+
/*
Get next page "token"
*/
@@ -1175,6 +1177,8 @@ class brave{
// load html
$this->fuckhtml->load($html);
+ $this->detect_captcha($html);
+
// get npt
$out["npt"] =
$this->generatenextpagetoken(
@@ -1270,6 +1274,9 @@ class brave{
fclose($handle);*/
$this->fuckhtml->load($html);
+
+ $this->detect_captcha($html);
+
$json = $this->get_js();
foreach(
@@ -1374,6 +1381,8 @@ class brave{
$this->fuckhtml->load($html);
+ $this->detect_captcha($html);
+
$out = [
"status" => "ok",
"npt" => null,
@@ -1853,4 +1862,29 @@ class brave{
)
);
}
+
+ private function detect_captcha(){
+
+ $title =
+ $this->fuckhtml
+ ->getElementsByTagName(
+ "title"
+ );
+
+ if(count($title) === 0){
+
+ throw new Exception("Brave returned a malformed page");
+ }
+
+ $title =
+ strtolower(
+ $this->fuckhtml
+ ->getTextContent($title[0])
+ );
+
+ if($title == "human verification"){
+
+ throw new Exception("Brave returned a Captcha");
+ }
+ }
}
diff --git a/scraper/qwant.php b/scraper/qwant.php
deleted file mode 100644
index 322182e..0000000
--- a/scraper/qwant.php
+++ /dev/null
@@ -1,1102 +0,0 @@
-backend = new backend("qwant");
- }
-
- public function getfilters($page){
-
- $base = [
- "nsfw" => [
- "display" => "NSFW",
- "option" => [
- "yes" => "Yes",
- "maybe" => "Maybe",
- "no" => "No"
- ]
- ],
- "country" => [
- "display" => "Country",
- "option" => [
- "en_US" => "United States",
- "fr_FR" => "France",
- "en_GB" => "Great Britain",
- "de_DE" => "Germany",
- "it_IT" => "Italy",
- "es_AR" => "Argentina",
- "en_AU" => "Australia",
- "es_ES" => "Spain (es)",
- "ca_ES" => "Spain (ca)",
- "cs_CZ" => "Czech Republic",
- "ro_RO" => "Romania",
- "el_GR" => "Greece",
- "zh_CN" => "China",
- "zh_HK" => "Hong Kong",
- "en_NZ" => "New Zealand",
- "fr_FR" => "France",
- "th_TH" => "Thailand",
- "ko_KR" => "South Korea",
- "sv_SE" => "Sweden",
- "nb_NO" => "Norway",
- "da_DK" => "Denmark",
- "hu_HU" => "Hungary",
- "et_EE" => "Estonia",
- "es_MX" => "Mexico",
- "es_CL" => "Chile",
- "en_CA" => "Canada (en)",
- "fr_CA" => "Canada (fr)",
- "en_MY" => "Malaysia",
- "bg_BG" => "Bulgaria",
- "fi_FI" => "Finland",
- "pl_PL" => "Poland",
- "nl_NL" => "Netherlands",
- "pt_PT" => "Portugal",
- "de_CH" => "Switzerland (de)",
- "fr_CH" => "Switzerland (fr)",
- "it_CH" => "Switzerland (it)",
- "de_AT" => "Austria",
- "fr_BE" => "Belgium (fr)",
- "nl_BE" => "Belgium (nl)",
- "en_IE" => "Ireland",
- "he_IL" => "Israel"
- ]
- ]
- ];
-
- switch($page){
-
- case "web":
- $base = array_merge(
- $base,
- [
- "time" => [
- "display" => "Time posted",
- "option" => [
- "any" => "Any time",
- "day" => "Past 24 hours",
- "week" => "Past week",
- "month" => "Past month"
- ]
- ],
- "extendedsearch" => [
- // no display, wont show in interface
- "option" => [
- "yes" => "Yes",
- "no" => "No"
- ]
- ]
- ]
- );
- break;
-
- case "images":
- $base = array_merge(
- $base,
- [
- "time" => [
- "display" => "Time posted",
- "option" => [
- "any" => "Any time",
- "day" => "Past 24 hours",
- "week" => "Past week",
- "month" => "Past month"
- ]
- ],
- "size" => [
- "display" => "Size",
- "option" => [
- "any" => "Any size",
- "large" => "Large",
- "medium" => "Medium",
- "small" => "Small"
- ]
- ],
- "color" => [
- "display" => "Color",
- "option" => [
- "any" => "Any color",
- "coloronly" => "Color only",
- "monochrome" => "Monochrome",
- "black" => "Black",
- "brown" => "Brown",
- "gray" => "Gray",
- "white" => "White",
- "yellow" => "Yellow",
- "orange" => "Orange",
- "red" => "Red",
- "pink" => "Pink",
- "purple" => "Purple",
- "blue" => "Blue",
- "teal" => "Teal",
- "green" => "Green"
- ]
- ],
- "imagetype" => [
- "display" => "Type",
- "option" => [
- "any" => "Any type",
- "animatedgif" => "Animated GIF",
- "photo" => "Photograph",
- "transparent" => "Transparent"
- ]
- ],
- "license" => [
- "display" => "License",
- "option" => [
- "any" => "Any license",
- "share" => "Non-commercial reproduction and sharing",
- "sharecommercially" => "Reproduction and sharing",
- "modify" => "Non-commercial reproduction, sharing and modification",
- "modifycommercially" => "Reproduction, sharing and modification",
- "public" => "Public domain"
- ]
- ]
- ]
- );
- break;
-
- case "videos":
- $base = array_merge(
- $base,
- [
- "order" => [
- "display" => "Order by",
- "option" => [
- "relevance" => "Relevance",
- "views" => "Views",
- "date" => "Most recent",
- ]
- ],
- "source" => [
- "display" => "Source",
- "option" => [
- "any" => "Any source",
- "youtube" => "YouTube",
- "dailymotion" => "Dailymotion",
- ]
- ]
- ]
- );
- break;
-
- case "news":
- $base = array_merge(
- $base,
- [
- "time" => [
- "display" => "Time posted",
- "option" => [
- "any" => "Any time",
- "hour" => "Less than 1 hour ago",
- "day" => "Past 24 hours",
- "week" => "Past week",
- "month" => "Past month"
- ]
- ],
- "order" => [
- "display" => "Order by",
- "option" => [
- "relevance" => "Relevance",
- "date" => "Most recent"
- ]
- ]
- ]
- );
- break;
- }
-
- return $base;
- }
-
- private function get($proxy, $url, $get = [], $is_post = false, $cookie = null){
-
- $curlproc = curl_init();
-
- if($is_post){
-
- $headers = [
- "User-Agent: " . config::USER_AGENT,
- "Accept: */*",
- "Accept-Language: en-US,en;q=0.9",
- "Accept-Encoding: gzip, deflate, br, zstd",
- "Referer: https://www.qwant.com/",
- "Content-type: application/x-www-form-urlencoded",
- "Origin: https://www.qwant.com",
- "DNT: 1",
- "Sec-GPC: 1",
- "Connection: keep-alive",
- "Sec-Fetch-Dest: empty",
- "Sec-Fetch-Mode: cors",
- "Sec-Fetch-Site: same-site",
- "Pragma: no-cache",
- "Cache-Control: no-cache"
- ];
-
- curl_setopt($curlproc, CURLOPT_POST, true);
- curl_setopt($curlproc, CURLOPT_POSTFIELDS, http_build_query($get));
- }else{
-
- $headers = [
- "User-Agent: " . config::USER_AGENT,
- "Accept: application/json, text/plain, */*",
- "Accept-Language: en-US,en;q=0.9",
- "Accept-Encoding: gzip, deflate, br, zstd",
- "Referer: https://www.qwant.com/",
- "Origin: https://www.qwant.com",
- "DNT: 1",
- ($cookie !== null ? "Cookie: {$cookie}" : ""),
- "Sec-GPC: 1",
- "Connection: keep-alive",
- "Sec-Fetch-Dest: empty",
- "Sec-Fetch-Mode: cors",
- "Sec-Fetch-Site: same-site",
- "Priority: u=4",
- "Pragma: no-cache",
- "Cache-Control: no-cache",
- "TE: trailers"
- ];
-
- if($get !== []){
- $get = http_build_query($get);
- $url .= "?" . $get;
- }
- }
-
- curl_setopt($curlproc, CURLOPT_URL, $url);
-
- curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
- curl_setopt($curlproc, CURLOPT_HTTPHEADER, $headers);
-
- // Bypass HTTP/2 check
- curl_setopt($curlproc, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
-
- curl_setopt($curlproc, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($curlproc, CURLOPT_SSL_VERIFYHOST, 2);
- curl_setopt($curlproc, CURLOPT_SSL_VERIFYPEER, true);
- curl_setopt($curlproc, CURLOPT_CONNECTTIMEOUT, 30);
- curl_setopt($curlproc, CURLOPT_TIMEOUT, 30);
-
- $this->backend->assign_proxy($curlproc, $proxy);
-
- $data = curl_exec($curlproc);
-
- if(curl_errno($curlproc)){
- throw new Exception(curl_error($curlproc));
- }
-
- curl_close($curlproc);
-
- return $data;
- }
-
- public function web($get){
-
- if($get["npt"]){
-
- // get next page data
- [$params, $proxy] = $this->backend->get($get["npt"], "web");
-
- $params = json_decode($params, true);
- $cookie = $params["cookie"];
- unset($params["cookie"]);
-
- }else{
-
- // get _GET data instead
- $search = $get["s"];
- if(strlen($search) === 0){
-
- throw new Exception("Search term is empty!");
- }
-
- if(strlen($search) > 2048){
-
- throw new Exception("Search term is too long!");
- }
-
- $proxy = $this->backend->get_ip();
-
- $params = [
- "q" => $search,
- "freshness" => $get["time"],
- "count" => 10,
- "locale" => $get["country"],
- "offset" => 0,
- "device" => "desktop",
- "tgp" => 3,
- "safesearch" => 0,
- "displayed" => "true",
- "llm" => "false"
- ];
-
- switch($get["nsfw"]){
-
- case "yes": $params["safesearch"] = 0; break;
- case "maybe": $params["safesearch"] = 1; break;
- case "no": $params["safesearch"] = 2; break;
- }
-
- $cookie = null;
- }
- /*
- $handle = fopen("scraper/qwant_web.json", "r");
- $json = fread($handle, filesize("scraper/qwant_web.json"));
- fclose($handle);*/
-
- $cookie = $this->fuck_datadome($proxy, $search);
-
- try{
- $json =
- $this->get(
- $proxy,
- "https://api.qwant.com/v3/search/web",
- $params,
- false,
- $cookie
- );
-
- }catch(Exception $error){
-
- throw new Exception("Could not fetch JSON");
- }
-
- $json = json_decode($json, true);
-
- if($json === NULL){
-
- throw new Exception("Failed to decode JSON");
- }
-
- if(isset($json["data"]["message"][0])){
-
- throw new Exception("Server returned an error:\n" . $json["data"]["message"][0]);
- }
-
- $out = [
- "status" => "ok",
- "spelling" => [
- "type" => "no_correction",
- "using" => null,
- "correction" => null
- ],
- "npt" => null,
- "answer" => [],
- "web" => [],
- "image" => [],
- "video" => [],
- "news" => [],
- "related" => []
- ];
-
- $this->detect_errors($json);
-
- if(
- $json["status"] != "success" &&
- $json["data"]["error_code"] === 5
- ){
-
- // no results
- return $out;
- }
-
- if(!isset($json["data"]["result"]["items"]["mainline"])){
-
- throw new Exception("Server did not return a result object");
- }
-
- // data is OK, parse
-
- // get instant answer
- if(
- $get["extendedsearch"] == "yes" &&
- isset($json["data"]["result"]["items"]["sidebar"][0]["endpoint"])
- ){
-
- try{
- $answer =
- $this->get(
- $proxy,
- "https://api.qwant.com/v3" .
- $json["data"]["result"]["items"]["sidebar"][0]["endpoint"],
- []
- );
-
- $answer = json_decode($answer, true);
-
- if(
- $answer === null ||
- $answer["status"] != "success" ||
- $answer["data"]["result"] === null
- ){
-
- throw new Exception();
- }
-
- // parse answer
- $out["answer"][] = [
- "title" => $answer["data"]["result"]["title"],
- "description" => [
- [
- "type" => "text",
- "value" => $this->trimdots($answer["data"]["result"]["description"])
- ]
- ],
- "url" => $answer["data"]["result"]["url"],
- "thumb" =>
- $answer["data"]["result"]["thumbnail"]["landscape"] == null ?
- null :
- $this->unshitimage($answer["data"]["result"]["thumbnail"]["landscape"]),
- "table" => [],
- "sublink" => []
- ];
-
- }catch(Exception $error){
-
- // do nothing in case of failure
- }
-
- }
-
- // get word correction
- if(isset($json["data"]["query"]["queryContext"]["alteredQuery"])){
-
- $out["spelling"] = [
- "type" => "including",
- "using" => $json["data"]["query"]["queryContext"]["alteredQuery"],
- "correction" => $json["data"]["query"]["queryContext"]["alterationOverrideQuery"]
- ];
- }
-
- // check for next page
- if($json["data"]["result"]["lastPage"] === false){
-
- $params["offset"] = $params["offset"] + 10;
- $params["cookie"] = $cookie;
-
- $out["npt"] =
- $this->backend->store(
- json_encode($params),
- "web",
- $proxy
- );
- }
-
- // parse results
- foreach($json["data"]["result"]["items"]["mainline"] as $item){
-
- switch($item["type"]){ // ignores ads
-
- case "web":
-
- $first_iteration = true;
- foreach($item["items"] as $result){
-
- if(isset($result["thumbnailUrl"])){
-
- $thumb = [
- "url" => $this->unshitimage($result["thumbnailUrl"]),
- "ratio" => "16:9"
- ];
- }else{
-
- $thumb = [
- "url" => null,
- "ratio" => null
- ];
- }
-
- $sublinks = [];
- if(isset($result["links"])){
-
- foreach($result["links"] as $link){
-
- $sublinks[] = [
- "title" => $this->trimdots($link["title"]),
- "date" => null,
- "description" => isset($link["desc"]) ? $this->trimdots($link["desc"]) : null,
- "url" => $link["url"]
- ];
- }
- }
-
- // detect gibberish results
- if(
- $first_iteration &&
- !isset($result["urlPingSuffix"])
- ){
-
- throw new Exception("Qwant returned gibberish results");
- }
-
- $out["web"][] = [
- "title" => $this->trimdots($result["title"]),
- "description" => $this->trimdots($result["desc"]),
- "url" => $result["url"],
- "date" => null,
- "type" => "web",
- "thumb" => $thumb,
- "sublink" => $sublinks,
- "table" => []
- ];
-
- $first_iteration = false;
- }
- break;
-
- case "images":
- foreach($item["items"] as $image){
-
- $out["image"][] = [
- "title" => $image["title"],
- "source" => [
- [
- "url" => $image["media"],
- "width" => (int)$image["width"],
- "height" => (int)$image["height"]
- ],
- [
- "url" => $this->unshitimage($image["thumbnail"]),
- "width" => $image["thumb_width"],
- "height" => $image["thumb_height"]
- ]
- ],
- "url" => $image["url"]
- ];
- }
- break;
-
- case "videos":
- foreach($item["items"] as $video){
-
- $out["video"][] = [
- "title" => $video["title"],
- "description" => null,
- "date" => (int)$video["date"],
- "duration" => $video["duration"] === null ? null : $video["duration"] / 1000,
- "views" => null,
- "thumb" =>
- $video["thumbnail"] === null ?
- [
- "url" => null,
- "ratio" => null,
- ] :
- [
- "url" => $this->unshitimage($video["thumbnail"]),
- "ratio" => "16:9",
- ],
- "url" => $video["url"]
- ];
- }
- break;
-
- case "related_searches":
- foreach($item["items"] as $related){
-
- $out["related"][] = $related["text"];
- }
- break;
- }
- }
-
- return $out;
- }
-
-
- public function image($get){
-
- if($get["npt"]){
-
- [$params, $proxy] =
- $this->backend->get(
- $get["npt"],
- "images"
- );
-
- $params = json_decode($params, true);
- $cookie = $params["cookie"];
- unset($params["cookie"]);
-
- }else{
-
- $search = $get["s"];
-
- if(strlen($search) === 0){
-
- throw new Exception("Search term is empty!");
- }
-
- $proxy = $this->backend->get_ip();
-
- $params = [
- "t" => "images",
- "q" => $search,
- "count" => 125,
- "locale" => $get["country"],
- "offset" => 0, // increment by 125
- "device" => "desktop",
- "tgp" => 3
- ];
-
- if($get["time"] != "any"){
-
- $params["freshness"] = $get["time"];
- }
-
- foreach(["size", "color", "imagetype", "license"] as $p){
-
- if($get[$p] != "any"){
-
- $params[$p] = $get[$p];
- }
- }
-
- switch($get["nsfw"]){
-
- case "yes": $params["safesearch"] = 0; break;
- case "maybe": $params["safesearch"] = 1; break;
- case "no": $params["safesearch"] = 2; break;
- }
-
- $cookie = null;
- }
-
- $cookie = $this->fuck_datadome($proxy, $search);
-
- try{
- $json = $this->get(
- $proxy,
- "https://api.qwant.com/v3/search/images",
- $params,
- false,
- $cookie
- );
- }catch(Exception $err){
-
- throw new Exception("Failed to get JSON");
- }
-
- /*
- $handle = fopen("scraper/yandex.json", "r");
- $json = fread($handle, filesize("scraper/yandex.json"));
- fclose($handle);*/
-
- $json = json_decode($json, true);
-
- if($json === null){
-
- throw new Exception("Failed to decode JSON");
- }
-
- $this->detect_errors($json);
-
- if(isset($json["data"]["result"]["items"]["mainline"])){
-
- throw new Exception("Qwant returned gibberish results");
- }
-
- $out = [
- "status" => "ok",
- "npt" => null,
- "image" => []
- ];
-
- if($json["data"]["result"]["lastPage"] === false){
-
- $params["offset"] = $params["offset"] + 125;
-
- $out["npt"] = $this->backend->store(
- json_encode($params),
- "images",
- $proxy
- );
- }
-
- foreach($json["data"]["result"]["items"] as $image){
-
- $out["image"][] = [
- "title" => $this->trimdots($image["title"]),
- "source" => [
- [
- "url" => $image["media"],
- "width" => $image["width"],
- "height" => $image["height"]
- ],
- [
- "url" => $this->unshitimage($image["thumbnail"]),
- "width" => $image["thumb_width"],
- "height" => $image["thumb_height"]
- ]
- ],
- "url" => $image["url"]
- ];
- }
-
- return $out;
- }
-
- public function video($get){
-
- $search = $get["s"];
- if(strlen($search) === 0){
-
- throw new Exception("Search term is empty!");
- }
-
- $params = [
- "t" => "videos",
- "q" => $search,
- "count" => 50,
- "locale" => $get["country"],
- "offset" => 0, // dont implement pagination
- "device" => "desktop",
- "tgp" => 3
- ];
-
- switch($get["nsfw"]){
-
- case "yes": $params["safesearch"] = 0; break;
- case "maybe": $params["safesearch"] = 1; break;
- case "no": $params["safesearch"] = 2; break;
- }
-
- try{
- $json =
- $this->get(
- $this->backend->get_ip(),
- "https://api.qwant.com/v3/search/videos",
- $params
- );
- }catch(Exception $error){
-
- throw new Exception("Could not fetch JSON");
- }
-
- /*
- $handle = fopen("scraper/yandex-video.json", "r");
- $json = fread($handle, filesize("scraper/yandex-video.json"));
- fclose($handle);
- */
-
- $json = json_decode($json, true);
-
- if($json === null){
-
- throw new Exception("Could not parse JSON");
- }
-
- $this->detect_errors($json);
-
- if(isset($json["data"]["result"]["items"]["mainline"])){
-
- throw new Exception("Qwant returned gibberish results");
- }
-
- $out = [
- "status" => "ok",
- "npt" => null,
- "video" => [],
- "author" => [],
- "livestream" => [],
- "playlist" => [],
- "reel" => []
- ];
-
- foreach($json["data"]["result"]["items"] as $video){
-
- if(empty($video["thumbnail"])){
-
- $thumb = [
- "url" => null,
- "ratio" => null
- ];
- }else{
-
- $thumb = [
- "url" => $this->unshitimage($video["thumbnail"]),
- "ratio" => "16:9"
- ];
- }
-
- $duration = (int)$video["duration"];
-
- $out["video"][] = [
- "title" => $video["title"],
- "description" => $this->limitstrlen($video["desc"]),
- "author" => [
- "name" => $video["channel"],
- "url" => null,
- "avatar" => null
- ],
- "date" => (int)$video["date"],
- "duration" => $duration === 0 ? null : $duration,
- "views" => null,
- "thumb" => $thumb,
- "url" => preg_replace("/\?syndication=.+/", "", $video["url"])
- ];
- }
-
- return $out;
- }
-
- public function news($get){
-
- $search = $get["s"];
- if(strlen($search) === 0){
-
- throw new Exception("Search term is empty!");
- }
-
- $params = [
- "t" => "news",
- "q" => $search,
- "count" => 50,
- "locale" => $get["country"],
- "offset" => 0, // dont implement pagination
- "device" => "desktop",
- "tgp" => 3
- ];
-
- switch($get["nsfw"]){
-
- case "yes": $params["safesearch"] = 0; break;
- case "maybe": $params["safesearch"] = 1; break;
- case "no": $params["safesearch"] = 2; break;
- }
-
- try{
- $json =
- $this->get(
- $this->backend->get_ip(),
- "https://api.qwant.com/v3/search/news",
- $params
- );
- }catch(Exception $error){
-
- throw new Exception("Could not fetch JSON");
- }
-
- /*
- $handle = fopen("scraper/yandex-video.json", "r");
- $json = fread($handle, filesize("scraper/yandex-video.json"));
- fclose($handle);
- */
-
- $json = json_decode($json, true);
-
- if($json === null){
-
- throw new Exception("Could not parse JSON");
- }
-
- $this->detect_errors($json);
-
- if(isset($json["data"]["result"]["items"]["mainline"])){
-
- throw new Exception("Qwant returned gibberish results");
- }
-
- $out = [
- "status" => "ok",
- "npt" => null,
- "news" => []
- ];
-
- foreach($json["data"]["result"]["items"] as $news){
-
- if(empty($news["media"][0]["pict_big"]["url"])){
-
- $thumb = [
- "url" => null,
- "ratio" => null
- ];
- }else{
-
- $thumb = [
- "url" => $this->unshitimage($news["media"][0]["pict_big"]["url"]),
- "ratio" => "16:9"
- ];
- }
-
- $out["news"][] = [
- "title" => $news["title"],
- "author" => $news["press_name"],
- "description" => $this->trimdots($news["desc"]),
- "date" => (int)$news["date"],
- "thumb" => $thumb,
- "url" => $news["url"]
- ];
- }
-
- return $out;
- }
-
- private function detect_errors($json){
-
- if(
- isset($json["status"]) &&
- $json["status"] == "error"
- ){
-
- if(isset($json["data"]["error_data"]["captchaUrl"])){
-
- throw new Exception("Qwant returned a captcha");
- }elseif(isset($json["data"]["error_data"]["error_code"])){
-
- throw new Exception(
- "Qwant returned an API error: " .
- $json["data"]["error_data"]["error_code"]
- );
- }
-
- throw new Exception("Qwant returned an API error");
- }
-
- if(
- isset($json["url"]) &&
- preg_match(
- '/captcha/i',
- $json["url"]
- )
- ){
-
- throw new Exception("Qwant returned a captcha redirect");
- }
- }
-
- private function limitstrlen($text){
-
- return explode("\n", wordwrap($text, 300, "\n"))[0];
- }
-
- private function trimdots($text){
-
- return trim($text, ". ");
- }
-
- private function unshitimage($url){
-
- // https://s1.qwant.com/thumbr/0x0/8/d/f6de4deb2c2b12f55d8bdcaae576f9f62fd58a05ec0feeac117b354d1bf5c2/th.jpg?u=https%3A%2F%2Fwww.bing.com%2Fth%3Fid%3DOIP.vvDWsagzxjoKKP_rOqhwrQAAAA%26w%3D160%26h%3D160%26c%3D7%26pid%3D5.1&q=0&b=1&p=0&a=0
- // https://s2.qwant.com/thumbr/474x289/7/f/412d13b3fe3a03eb2b89633c8e88b609b7d0b93cdd9a5e52db3c663e41e65e/th.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%3Fid%3DOIP.9Tm_Eo6m7V7ltN19mxduDgHaEh%26pid%3DApi&q=0&b=1&p=0&a=0
-
- $image = parse_url($url);
-
- if(
- !isset($image["host"]) ||
- !isset($image["query"])
- ){
-
- // cant do anything
- return $url;
- }
-
- $id = null;
-
- if(
- preg_match(
- '/s[0-9]+\.qwant\.com$/',
- $image["host"]
- )
- ){
-
- parse_str($image["query"], $str);
-
- // we're being served a proxy URL
- if(isset($str["u"])){
-
- $bing_url = $str["u"];
- }else{
-
- // give up
- return $url;
- }
- }
-
- // parse bing URL
- $id = null;
- $image = parse_url($bing_url);
-
- if(isset($image["query"])){
-
- parse_str($image["query"], $str);
-
- if(isset($str["id"])){
-
- $id = $str["id"];
- }
- }
-
- if($id === null){
-
- $id = explode("/th/id/", $image["path"], 2);
-
- if(count($id) !== 2){
-
- // malformed
- return $url;
- }
-
- $id = $id[1];
- }
-
- if(is_array($id)){
-
- // fuck off, let proxy.php deal with it
- return $url;
- }
-
- return "https://" . $image["host"] . "/th?id=" . rawurlencode($id);
- }
-
- private function fuck_datadome($proxy, $search){
-
- if($cookie === null){
- // get datadome bullshit
- // yeah go ahead and patch this you code monkey
-
- // ... you know, instead of fixing this shit, can you make the second page of results return more than 1 result?
- // i search shit like "higurashi" and i only get 1 result and then just a bunch of related searches ad finitum
- // also, stop pretending you guys have your own index, this is clearly all bing results
- try{
- $datadome =
- $this->get(
- $proxy,
- "https://dd.qwant.com/js/",
- [
- "jspl" => "-oirHDeP7G3G9LXY3rhxGDYf6t_ZlYPzaiL2dRhQ34uNUc4dO2Td0SmmKu_VJp7-jrAIoOMrTqKgCtV4cPzntMyNuOYN7QQ1q0Fi2c-679vYpJr9ub5dRFXMhCMQmjZsOWXUa8yafIXf85UA5Goct-n4hVT5dFfBGM-dE6gwZEFkHQ1j1195sLybZFTNA1IfX26MBMxlVHoyT-aHtkd6YjGXbvOPMMsObVHDa_Cf4BmApRGo-eehsAttsCqDRwkl0M2E1b0OXCIwMVnCK-qWZ81MGaJGjqxdj_LaxhAjE9JI_4oNedAg4Yj4m_vkQBKdmWJT_bTUsllAZXqX5IxYQHJ8WvlknOTr6sR3wgoSOa_JNVHkUQxqXBbQcoDdy6VciL5rIqwWl4Y2caVYzeXa8YNXxwsfrPDegH3dQ6vOiIiCDT3bexkcS_y7GAioPPv9qDu7M9y4sI8t0VkPgMbWY6MIu5u9WAv6lEiKIYrtlyU-iiXnNdkd9EnpC1TD8_s12PtCuLg5bGgLVmI1dw8VXVZxmVyFvfBtuqXkzM-kZOyv-YIRcf-wPY8DUtnHox1d6t64oYhGv-pYV5dmZ0rGs99LC6ThiifeF_oHz5ZK46Ms68WsiAjc1HLAbFPFxWw8X6lyLlpT5aHgFdDBeCTExKXyItlc9PBxkgK8UnXHQFiWX3HTen8rziwkI31AbkMU7xGP6cvuwuLG7MA7sH3yLF1uimQlhr6EuTrImWQNV7X81DqyZ0OE7I-uT9bp_xISa5it-6AJpFtGcwz04_OuO2zvZazBeiKJI5Fw0X2r3RHTHdA29SXqTIe4y2LRAAp0MSAZUIJAMIbbrz9BG-3D5BHfQUA1Bbuklxv-tNWiuanWbbTmAiuxTAo9P-D2ZMrWHwdN8sCb3nINLur38z1xviSs2qEEjKo9kzQIWoHgwm_amV-jjDtNBXwpCQnoaqt3FfL8e59ecYWVg61Gnl3YCwq6rubFdkIv6UStcWSlj4pRFlFZSjQxzznvGTAc5Nz1JJWVeBHJItei4wqDUYEw2BNTS_fU9keV3LFyKDH_x248AxHLiXt583DhZZwPAKlpmMo0E7odzfOgVQGM5b9l64NL3RQRq8cDxI8KgmOFnPHdI7Myq1A3nW1uoBB8m9_GR_9orboKl5tAQrT0VTMRd6JyNwOOQ71ZoQpMwsD5Xd9YDZd618TNE9dA38h9OyBco_-hwKCYayyEqib1atLLBO_gnO4InR471AklOokDOKx1oY_xgNhlFrZskE4nQQOqeCQ_47oysob9s3kblA8UqDrL2vM3i2KPLWwlQzMAPrQAIv7Amp6rO-wYwBQdbP7XghYympxx9-hNxc1Duq-nS4R3pjPtZg3LoCvG1N_0JLf9jfPBqw_rdPM6DnjEOvpCdvrfv4wBlEpgqbaRQCZBPEz-O5ZlY24OyPvaNX0krPeXkbi-KYuwZSAbfW0zsksXb8cbRmz6tvh25IRYP1XNDPVd8NFrYOdDH9GzhV5MYZziYMurQlVwgumGLz85Y6qTHZU2D5qkMH7o5C5bzptReO96889oxkReS_LDhzdxH0tAQ5a9GgETJTHAXJ9Lt7X037i0qQlatbPLizOl9fo4vUBGJ7w-uSUmIzg1yIwOo8xhLu39WghXfoJ_EX5ptNi7arpAo11UoJIGEgWgq7bS40q2dtnG5HYpp9ef37iFgztM7PV5dlJf_W-n9uXJyCk2nhxZeuGiIGhMTzjP_8oeyiMFelmoyLlqK8wOs6hvWqgDD4NwtZc-XBkEAKsxuPrirb_sszp0z6BWsh3D6fBxAW_lTrNVkxZ1EBtoEN72HvO1aeX8PVuJP3CKwkkUcAhJ5Zvajo8t4Ai8ave2MTZul4QhDIe7ZALJMo71u-hNSj8YG9JLPqF1jcnbkl8ykt_XMMxX8wSZ_AfTsfmAuOWBfEzxv8Fl2Ne-oxcEtfuYnYNJIjQDHXXdtYbWKBezE41wonQu7FWh3wSSF3DDm9EocWMsUZ610nrIviOFdtTqjrY7KnOZp0hS9Hm2V04z-06Db-To71L5O5poYKuOlDAP_ULfW0hgMgYaPnt4Nive9gtue6ZQ5yNFylKM11zCWcF8vmuEPGx4NIZjvNFB-QSkdxZtBTQOgO6r4BqHJNZIDqiD9A6PyMemVtQEo-JhooX6bwHFHx6C84r-kKq1lg0RaDelbu1h6347kocxOczdgHWlagKPFrUvHpPHvCZeFmzLLmPWbDEpUhhfkbEZlNe47svO5WN1TYZMCAlE0g01pe6b_NB9-twQ4FJB1SkUQRA4o5DxiIHSgXBBfw1X98XPEKcJgCU2zRbYuU69mvyPcsxoPsLo8Cl2OAXj2U1tcLUxVhR_vu3I0SG_Ee8hxBp5b6QbpIu2sE0bgpqfi88XqUXf3qvxGbkoZDZ8dPvEvCVmpDZOOGJY5Db4MnpKBByW79lUnpbLCbsVOoxYu8SVmVQ63eGY9yBlACuWxlXA_PhCpnKYwMppjxKvKqalNJGMMg5WQ8cvhsIftE5wgaj21n0A15GwRO1WgUcsO_dLe1MqO88hTHWHV0Q3gNut3sdsmVr_n-0SniLtVAaw4IEtDZWNUfhpaoC7iyKxoJ86t8r697EYPlWUu4nTQhb3TOXAjHF22rFsJJMldKHVxjxjd086ky3OY3L_7cbjVof60-y4gTJdumw5hch-mhaT93nVnaTuPLGCAC48dZSgOitW8Dw62mPS05fTPAfKUY1HpyIWr9ACk7U2KVAijpzFB-bXvqJeCyBeUHr4cB61K2XxTmkNA8Oo9RC32TeYDrThFzoUyYZ_D6zPe1lLtQaFsPRvDBSsYCcr5fcABlST4DyUgnkgqtx-0-D1QEwtWiH3xiHLQGa82ldjqNIXL9LqwxYoCRBKuljHyqEylZD26A9Jp6SL9rk_Ol6TJlYa3zx44PHyMoA3Coy-HIZW5NMwVKdV7egUDDOchvuFnc1Zv2QpSFKc0JY8lYFtSm1HZFwjq7hf2hBqkU0Xneh8AT4Ty9QVb4mJ8rpzc8m_NBZNrpZVE-pIzmv2yGjnRwKSrwsjat7b2iY34UPFqGBlDk5FX6EfRlN9bq-nztWdextLc47ndr1gxsjC-vr6tbdXl5FxMVhbsnLH80wOC6hHubxCysr5C7Ft59s3MLhVQCc3IIOPMBhLOjjjVsu6K8kAJvvamIpj-iYUQj9T8JztAqfobytzbiwPsQ3mywUGkJpZxTjXjqkKdT6yfFdW5ksR8p3VlKGAAKft9TKKcc5IgErmIRtiZF4hGhB45nwIO5XHA59V94QuV3b0oJrleuDCBj0iDFqWYpsUVHyKl3RjrO86Yq1IbNRwXIIMrlsVYos9CS1I75k9kW4yQC6ni3a2ZitDkiEB-SoQuTlBsvfTs892Wt5q3CWH6_7srZO6l2QEAlWqyQp_P4i8j8uiEbZksGsgghVIgOkriblDP9ml7PdkpRX7S3OpY7jjQgTNR8XHaZ-SEw0-zPiqIIt5VioFK8JljPn-nMIDx-ew_vTDKqmKEOF39ezQcc5oQNTQNUvTAe8qCbK4ghS3fagItezoHtB5zm3qwhx9Iu6JMJy0UhywUZBaInY6NokiVZGDgtoqlVjOk7K4299MVZgsOr2P9QHRDo41lYnCeWWFUf50d-ouEOUO_ngteDpgG1DfV64eycrUOWAnpfI8XOUZ7mxS89SqJriw3T3tcljowHiOFw9QjYsjJi0vuHL5jfoBZVdYAUH6KiLfIScO-KgqitdUTpFwks8Qk7xfd2HcEfArlzSiAt3Hmb2Ty-5mhvtQj2RbPehgoI7gqa60b6Qffth-5tbIjXoKlZCQsaKVMAggF9C3w0Rv5uJ3ExCTGsfKO9IVOf--AfxaA8cNoO0W3NTaMH2iWftY0O1-1x3VJEcerp2v9BcZ0iIL3VFJ9eyOPYNHNwTYrI3S6OrVZxuQXFbu2E_Us32wxYqXvnMx-DEOk7bUUFIanL69Cbn_mR_9Wm1sFixBQ15diJoe1Oyl3XWHWL6_YF4XnHzZlMWySt3Awdeynwu99Ter8WMGXl-6K30novKRWC9jDVOfAtxPvXBkkRtpm8nnJT8fwkmIQoWOEPT2MCg3VYzzHzVEH-AueuRiCZj_o7L-fRqbtFrWPyVnGuetUYS5u0D3n7Rr6kCVqWhLfwjqAwV_calzIQs8-J0-AJe6GGaaONPYfXwXjYan0Sl11CS64lXvx7i3MgeOZuH210T6g6hj54dSLw0upO1L8k0FGli3I5OhLMekeGMLT_N2akyHktjbz8Odpk2ms0pJP_PdqvUegjXgH81F5lr40HAXZs7jfC9ytgcWaEjwlmnoFHSCTdlCIgJqEBzItXiz6ilk5zP35VwxdQTNoms7Meqg-ZrgRuLRaqXNtpQT9qWBmiFP6zoYLwymqytZ_P1SV9DCM-KIZQ2Cq-ew8PsYSrwl8GmfuegoKEMBiON1I4mC9ChDgzJmbo5P9te60T0URS5g9ExiQuSo-JXs0MKC57LLiNo3MPZvqR1wRJOTg0_FqS4JFxzPwN5OqhBzfs25f6kK-axf0wWp-LetUx3U1CTNaxfbCTK5tb8KnLOIs3lDwJw5H5wIIRgXnfNLBJT8A2srfXkdpA",
- "eventCounters" => [],
- "jsType" => "ch",
- "cid" => "3i4vPGHCl74uIBumWaxF2a0PjFkyCJxcSvFnn~heX7keA6sGHgRbnzSYcMr_RzJFlfeeBdg9HeufzFPxnvSHbyWowSvjm6mNCSPhkx6MwDqptANoj93ZgGVbrzxGtcYe",
- "ddk" => "78B13B7513D180B7AB6D6FF9EB0A51",
- "Referer" => "https://www.qwant.com/?q={$search}&t=web",
- "request" => "/?q={$search}&t=web",
- "responsePage" => "origin",
- "ddv" => "5.8.0"
- ],
- true
- );
- }catch(Exception $error){
-
- throw new Exception("Failed to fetch JS challenge token");
- }
-
- $datadome = json_decode($datadome, true);
- if($datadome === null){
-
- throw new Exception("Failed to decode JS challenge JSON");
- }
-
- if(!isset($datadome["cookie"])){
-
- throw new Exception("Failed to get cookie from JS challenge endpoint");
- }
-
- $cookie = explode(";", $datadome["cookie"])[0];
- }
-
- return $cookie;
- }
-}
diff --git a/scraper/startpage.php b/scraper/startpage.php
index 2dd3cbc..2d94f3f 100644
--- a/scraper/startpage.php
+++ b/scraper/startpage.php
@@ -9,6 +9,10 @@ class startpage{
include "lib/fuckhtml.php";
$this->fuckhtml = new fuckhtml();
+
+ include "lib/anubis.php";
+ $this->anubis = new anubis();
+ $this->anubis_error = "all gucci";
}
public function getfilters($page){
@@ -244,7 +248,7 @@ class startpage{
}
}
- private function get($proxy, $url, $get = [], $post = false, $is_xhr = false){
+ private function get($proxy, $url, $get = [], $post = false, $is_xhr = false, $return_cookies = false, $passed_cookies = []){
$curlproc = curl_init();
@@ -259,6 +263,27 @@ class startpage{
$url .= "?" . $get;
}
+ if($return_cookies){
+
+ $cookies = [];
+ curl_setopt($curlproc, CURLOPT_HEADERFUNCTION, function($curlproc, $header) use (&$cookies){
+
+ $length = strlen($header);
+
+ $header = explode(":", $header, 2);
+
+ if(trim(strtolower($header[0])) == "set-cookie"){
+
+ $cookie_tmp = explode("=", trim($header[1]), 2);
+
+ $cookies[trim($cookie_tmp[0])] =
+ explode(";", $cookie_tmp[1], 2)[0];
+ }
+
+ return $length;
+ });
+ }
+
curl_setopt($curlproc, CURLOPT_URL, $url);
// http2 bypass
@@ -266,6 +291,15 @@ class startpage{
curl_setopt($curlproc, CURLOPT_ENCODING, ""); // default encoding
+ $passed_cookies["preferences"] = "date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEenglishN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius";
+
+ foreach($passed_cookies as $name => $val){
+
+ $header_cookies[] = "$name=$val";
+ }
+
+ $header_cookies = rtrim(implode("; ", $header_cookies));
+
if($is_xhr === true){
curl_setopt($curlproc, CURLOPT_HTTPHEADER,
@@ -279,7 +313,7 @@ class startpage{
"Origin: https://www.startpage.com/",
"DNT: 1",
"Connection: keep-alive",
- "Cookie: preferences=date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEenglishN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius",
+ "Cookie: $header_cookies",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: same-origin",
@@ -298,7 +332,7 @@ class startpage{
"Content-Length: " . strlen($get),
"DNT: 1",
"Connection: keep-alive",
- "Cookie: preferences=date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEenglishN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius",
+ "Cookie: $header_cookies",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
@@ -316,7 +350,7 @@ class startpage{
"Accept-Encoding: gzip",
"DNT: 1",
"Connection: keep-alive",
- "Cookie: preferences=date_timeEEEworldN1Ndisable_family_filterEEE1N1Ndisable_open_in_new_windowEEE0N1Nenable_post_methodEEE1N1Nenable_proxy_safety_suggestEEE0N1Nenable_stay_controlEEE0N1Ninstant_answersEEE1N1Nlang_homepageEEEs%2Fdevice%2FenN1NlanguageEEEenglishN1Nlanguage_uiEEEenglishN1Nnum_of_resultsEEE20N1Nsearch_results_regionEEEallN1NsuggestionsEEE1N1Nwt_unitEEEcelsius",
+ "Cookie: $header_cookies",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
@@ -342,6 +376,15 @@ class startpage{
}
curl_close($curlproc);
+
+ if($return_cookies){
+
+ return [
+ "cookies" => $cookies,
+ "data" => $data
+ ];
+ }
+
return $data;
}
@@ -349,14 +392,22 @@ class startpage{
if($get["npt"]){
+ $this->anubis_error = "Not ran on pagination";
[$post, $proxy] = $this->backend->get($get["npt"], "web");
+ $post = json_decode($post, true);
+ $query = $post["query"];
+ $cookies = $post["cookies"];
+
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $post,
- true
+ $query,
+ true,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
@@ -398,7 +449,11 @@ class startpage{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $params
+ $params,
+ false,
+ false,
+ true,
+ []
);
}catch(Exception $error){
@@ -406,12 +461,41 @@ class startpage{
}
//$html = file_get_contents("scraper/startpage.html");
+
+ // detect captcha on that page
+ $this->detect_captcha($html["data"]);
+
+ // solve anubis if need be
+ $cookies = $this->solve_anubis($proxy, $html);
+
+ if($cookies === false){
+
+ $cookies = [];
+ $html = $html["data"];
+ }else{
+
+ try{
+ $html = $this->get(
+ $proxy,
+ "https://www.startpage.com/sp/search",
+ $params,
+ false,
+ false,
+ false,
+ $cookies
+ );
+ }catch(Exception $error){
+
+ throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
+ }
+ }
}
-
- //echo htmlspecialchars($html);
+ // detect captcha even if we passed anubis challenge
$this->detect_captcha($html);
+ //echo htmlspecialchars($html);
+
if(
preg_match(
'/React\.createElement\(UIStartpage\.AppSerpWeb, ?(.+)\),?$/m',
@@ -420,14 +504,14 @@ class startpage{
) === 0
){
- throw new Exception("Failed to grep JSON object");
+ throw new Exception("Failed to grep JSON object. Anubis error: {$this->anubis_error}");
}
$json = json_decode($matches[1], true);
if($json === null){
- throw new Exception("Failed to decode JSON");
+ throw new Exception("Failed to decode JSON. Anubis error: {$this->anubis_error}");
}
//print_r($json);
@@ -449,7 +533,7 @@ class startpage{
];
// get npt
- $out["npt"] = $this->parse_npt($json, "web", $proxy);
+ $out["npt"] = $this->parse_npt($json, "web", $proxy, $cookies);
foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
@@ -658,14 +742,22 @@ class startpage{
if($get["npt"]){
+ $this->anubis_error = "Not ran on pagination";
[$post, $proxy] = $this->backend->get($get["npt"], "images");
+ $post = json_decode($post, true);
+ $query = $post["query"];
+ $cookies = $post["cookies"];
+
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $post,
- true
+ $query,
+ true,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
@@ -680,76 +772,102 @@ class startpage{
throw new Exception("Search term is empty!");
}
+ $proxy = $this->backend->get_ip();
+
+ $params = [
+ "query" => $get["s"],
+ "cat" => "images",
+ "pl" => "opensearch"
+ ];
+
+ if($get["nsfw"] == "no"){
+
+ $params["qadf"] = "heavy";
+ }
+
+ if($get["size"] != "any"){
+
+ if(
+ $get["size"] == "Small" ||
+ $get["size"] == "Medium" ||
+ $get["size"] == "Large" ||
+ $get["size"] == "Wallpaper"
+ ){
+
+ $params["flimgsize"] = $get["size"];
+ }else{
+
+ $params["image-size-select"] = "isz:lt,islt:" . $get["size"];
+ }
+ }
+
+ if($get["color"] != "any"){
+
+ if($get["color"] == "color"){
+
+ $params["flimgcolor"] = "ic:color";
+ }elseif($get["color"] == "bnw"){
+
+ $params["flimgcolor"] = "ic:gray";
+ }else{
+
+ $params["flimgcolor"] = "ic:specific,isc:" . $get["color"];
+ }
+ }
+
+ if($get["type"] != "any"){
+
+ $params["flimgtype"] = $get["type"];
+ }
+
+ if($get["license"] != "any"){
+
+ $params["flimglicense"] = $get["license"];
+ }
+
try{
+ $html = $this->get(
+ $proxy,
+ "https://www.startpage.com/sp/search",
+ $params,
+ false,
+ false,
+ true,
+ []
+ );
+ }catch(Exception $error){
- $proxy = $this->backend->get_ip();
+ throw new Exception("Failed to fetch search page");
+ }
+
+ //$html = file_get_contents("scraper/startpage.html");
+
+ // detect captcha on that page
+ $this->detect_captcha($html["data"]);
+
+ // solve anubis if need be
+ $cookies = $this->solve_anubis($proxy, $html);
+
+ if($cookies === false){
- $params = [
- "query" => $get["s"],
- "cat" => "images",
- "pl" => "opensearch"
- ];
-
- if($get["nsfw"] == "no"){
-
- $params["qadf"] = "heavy";
- }
-
- if($get["size"] != "any"){
-
- if(
- $get["size"] == "Small" ||
- $get["size"] == "Medium" ||
- $get["size"] == "Large" ||
- $get["size"] == "Wallpaper"
- ){
-
- $params["flimgsize"] = $get["size"];
- }else{
-
- $params["image-size-select"] = "isz:lt,islt:" . $get["size"];
- }
- }
-
- if($get["color"] != "any"){
-
- if($get["color"] == "color"){
-
- $params["flimgcolor"] = "ic:color";
- }elseif($get["color"] == "bnw"){
-
- $params["flimgcolor"] = "ic:gray";
- }else{
-
- $params["flimgcolor"] = "ic:specific,isc:" . $get["color"];
- }
- }
-
- if($get["type"] != "any"){
-
- $params["flimgtype"] = $get["type"];
- }
-
- if($get["license"] != "any"){
-
- $params["flimglicense"] = $get["license"];
- }
+ $cookies = [];
+ $html = $html["data"];
+ }else{
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $params
+ $params,
+ false,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
- throw new Exception("Failed to fetch search page");
+ throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
}
- //$html = file_get_contents("scraper/startpage.html");
-
- }catch(Exception $error){
-
- throw new Exception("Failed to fetch search page");
}
}
@@ -769,18 +887,18 @@ class startpage{
) === 0
){
- throw new Exception("Failed to grep JSON object");
+ throw new Exception("Failed to grep JSON object. Anubis error: {$this->anubis_error}");
}
$json = json_decode($matches[1], true);
if($json === null){
- throw new Exception("Failed to decode JSON object");
+ throw new Exception("Failed to decode JSON object. Anubis error: {$this->anubis_error}");
}
// get npt
- $out["npt"] = $this->parse_npt($json, "images", $proxy);
+ $out["npt"] = $this->parse_npt($json, "images", $proxy, $cookies);
// get images
foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
@@ -819,14 +937,22 @@ class startpage{
if($get["npt"]){
+ $this->anubis_error = "Not ran on pagination";
[$post, $proxy] = $this->backend->get($get["npt"], "videos");
+ $post = json_decode($post, true);
+ $query = $post["query"];
+ $cookies = $post["cookies"];
+
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $post,
- true
+ $query,
+ true,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
@@ -841,46 +967,72 @@ class startpage{
throw new Exception("Search term is empty!");
}
+ $proxy = $this->backend->get_ip();
+
+ $params = [
+ "query" => $get["s"],
+ "cat" => "video",
+ "pl" => "opensearch"
+ ];
+
+ if($get["nsfw"] == "no"){
+
+ $params["qadf"] = "heavy";
+ }
+
+ if($get["sort"] != "relevance"){
+
+ $params["sort_by"] = $get["sort"];
+ }
+
+ if($get["duration"] != "any"){
+
+ $params["with_duration"] = $get["duration"];
+ }
+
try{
+ $html = $this->get(
+ $proxy,
+ "https://www.startpage.com/sp/search",
+ $params,
+ false,
+ false,
+ true,
+ []
+ );
+ }catch(Exception $error){
- $proxy = $this->backend->get_ip();
+ throw new Exception("Failed to fetch search page");
+ }
+
+ //$html = file_get_contents("scraper/startpage.html");
+
+ // detect captcha on that page
+ $this->detect_captcha($html["data"]);
+
+ // solve anubis if need be
+ $cookies = $this->solve_anubis($proxy, $html);
+
+ if($cookies === false){
- $params = [
- "query" => $get["s"],
- "cat" => "video",
- "pl" => "opensearch"
- ];
-
- if($get["nsfw"] == "no"){
-
- $params["qadf"] = "heavy";
- }
-
- if($get["sort"] != "relevance"){
-
- $params["sort_by"] = $get["sort"];
- }
-
- if($get["duration"] != "any"){
-
- $params["with_duration"] = $get["duration"];
- }
+ $cookies = [];
+ $html = $html["data"];
+ }else{
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $params
+ $params,
+ false,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
- throw new Exception("Failed to fetch search page");
+ throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
}
- //$html = file_get_contents("scraper/startpage.html");
-
- }catch(Exception $error){
-
- throw new Exception("Failed to fetch search page");
}
}
@@ -894,14 +1046,14 @@ class startpage{
) === 0
){
- throw new Exception("Failed to get JSON object");
+ throw new Exception("Failed to get JSON object. Anubis error: {$this->anubis_error}");
}
$json = json_decode($matches[1], true);
if($json === null){
- throw new Exception("Failed to decode JSON object");
+ throw new Exception("Failed to decode JSON object. Anubis error: {$this->anubis_error}");
}
$out = [
@@ -915,7 +1067,7 @@ class startpage{
];
// get npt
- $out["npt"] = $this->parse_npt($json, "video", $proxy);
+ $out["npt"] = $this->parse_npt($json, "video", $proxy, $cookies);
// get results
foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
@@ -971,14 +1123,22 @@ class startpage{
if($get["npt"]){
+ $this->anubis_error = "Not ran on pagination";
[$post, $proxy] = $this->backend->get($get["npt"], "news");
+ $post = json_decode($post, true);
+ $query = $post["query"];
+ $cookies = $post["cookies"];
+
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $post,
- true
+ $query,
+ true,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
@@ -993,41 +1153,67 @@ class startpage{
throw new Exception("Search term is empty!");
}
+ $proxy = $this->backend->get_ip();
+
+ $params = [
+ "query" => $get["s"],
+ "cat" => "news",
+ "pl" => "opensearch"
+ ];
+
+ if($get["nsfw"] == "no"){
+
+ $params["qadf"] = "heavy";
+ }
+
+ if($get["time"] != "any"){
+
+ $params["with_date"] = $get["time"];
+ }
+
try{
+ $html = $this->get(
+ $proxy,
+ "https://www.startpage.com/sp/search",
+ $params,
+ false,
+ false,
+ true,
+ []
+ );
+ }catch(Exception $error){
- $proxy = $this->backend->get_ip();
+ throw new Exception("Failed to fetch search page. Anubis error: {$this->anubis_error}");
+ }
+
+ //$html = file_get_contents("scraper/startpage.html");
+
+ // detect captcha on that page
+ $this->detect_captcha($html["data"]);
+
+ // solve anubis if need be
+ $cookies = $this->solve_anubis($proxy, $html);
+
+ if($cookies === false){
- $params = [
- "query" => $get["s"],
- "cat" => "news",
- "pl" => "opensearch"
- ];
-
- if($get["nsfw"] == "no"){
-
- $params["qadf"] = "heavy";
- }
-
- if($get["time"] != "any"){
-
- $params["with_date"] = $get["time"];
- }
+ $cookies = [];
+ $html = $html["data"];
+ }else{
try{
$html = $this->get(
$proxy,
"https://www.startpage.com/sp/search",
- $params
+ $params,
+ false,
+ false,
+ false,
+ $cookies
);
}catch(Exception $error){
- throw new Exception("Failed to fetch search page");
+ throw new Exception("Failed to re-fetch search page. Anubis error: {$this->anubis_error}");
}
- //$html = file_get_contents("scraper/startpage.html");
-
- }catch(Exception $error){
-
- throw new Exception("Failed to fetch search page");
}
}
@@ -1041,14 +1227,14 @@ class startpage{
) === 0
){
- throw new Exception("Failed to get JSON object");
+ throw new Exception("Failed to get JSON object. Anubis error: {$this->anubis_error}");
}
$json = json_decode($matches[1], true);
if($json === null){
- throw new Exception("Failed to decode JSON object");
+ throw new Exception("Failed to decode JSON object. Anubis error: {$this->anubis_error}");
}
$out = [
@@ -1058,7 +1244,7 @@ class startpage{
];
// get npt
- $out["npt"] = $this->parse_npt($json, "news", $proxy);
+ $out["npt"] = $this->parse_npt($json, "news", $proxy, $cookies);
foreach($json["render"]["presenter"]["regions"]["mainline"] as $category){
@@ -1101,7 +1287,7 @@ class startpage{
return $out;
}
- private function parse_npt($json, $pagetype, $proxy){
+ private function parse_npt($json, $pagetype, $proxy, $cookies){
foreach($json["render"]["presenter"]["pagination"]["pages"] as $page){
@@ -1109,22 +1295,27 @@ class startpage{
return
$this->backend->store(
- http_build_query(
- [
- "lui" => "english",
- "language" => "english",
- "sc" => $json["render"]["search_sc"],
- "t" => "device",
- "cat" => $pagetype,
- "segment" => "organic",
- "abd" => "0",
- "abe" => "0",
- "query" => rawurldecode($json["render"]["query"]),
- "page" => $page["number"],
- "qsr" => "all",
- "qadf" => "none" // @ todo fix (??)
- ]
- ),
+ json_encode([
+ "query" =>
+ http_build_query(
+ [
+ "lui" => "english",
+ "language" => "english",
+ "sc" => $json["render"]["search_sc"],
+ "t" => "device",
+ "cat" => $pagetype,
+ "segment" => "startpage.udog", // "original" sometimes
+ "abd" => "0",
+ "abe" => "0",
+ "query" => rawurldecode($json["render"]["query"]),
+ "page" => $page["number"],
+ "qsr" => "all",
+ "qadf" => "none", // @ todo fix (??)
+ ...(isset($page["token"]) ? ["page_token" => $page["token"]] : [])
+ ]
+ ),
+ "cookies" => $cookies
+ ]),
$pagetype,
$proxy
);
@@ -1270,4 +1461,56 @@ class startpage{
throw new Exception("Startpage redirected the scraper to an unhandled page");
}
}
+
+ private function solve_anubis($proxy, $html){
+
+ //
+ // solve anubis challenge, if theres one
+ //
+ $res = null;
+ $cookies = [];
+
+ try{
+ $res = $this->anubis->scrape($html["data"]);
+
+ }catch(Exception $error){
+
+ // do nothang
+ $this->anubis_error = $error->getMessage();
+ }
+
+ if($res !== null){
+
+ // https://www.startpage.com/.within.website/x/cmd/anubis/api/pass-challenge
+ // ?id=01a00241-da22-7d62-b64f-e7a6a0e8bda8
+ // &response=0000460826adc23e0363e97e713e4aa36ca46e1a7cd933184442772f3d6b994c
+ // &nonce=3098
+ // &redir=https%3A%2F%2Fwww.startpage.com%2Fsp%2Fsearch
+ // &elapsedTime=111
+
+ $anubis_response =
+ $this->get(
+ $proxy,
+ "https://www.startpage.com/.within.website/x/cmd/anubis/api/pass-challenge",
+ [
+ "id" => $res["id"],
+ "response" => $res["response"],
+ "nonce" => $res["nonce"],
+ "redir" => "https://www.startpage.com/sp/search",
+ "elapsedTime" => $res["elapsedTime"]
+ ],
+ false,
+ false,
+ true,
+ $html["cookies"]
+ );
+
+ // re-fetch search page AGAIN with the anubis bullshit
+ $cookies = array_merge($anubis_response["cookies"], $html["cookies"]);
+
+ return $cookies;
+ }
+
+ return false;
+ }
}
\ No newline at end of file
diff --git a/settings.php b/settings.php
index 323bfd3..55c7f78 100644
--- a/settings.php
+++ b/settings.php
@@ -87,10 +87,6 @@ $settings = [
"value" => "kagi",
"text" => "Kagi"
],
- [
- "value" => "qwant",
- "text" => "Qwant"
- ],
[
"value" => "yep",
"text" => "Yep"
@@ -145,10 +141,6 @@ $settings = [
"value" => "startpage",
"text" => "Startpage"
],
- [
- "value" => "qwant",
- "text" => "Qwant"
- ],
[
"value" => "ghostery",
"text" => "Ghostery"
@@ -231,10 +223,6 @@ $settings = [
"value" => "startpage",
"text" => "Startpage"
],
- [
- "value" => "qwant",
- "text" => "Qwant"
- ],
[
"value" => "naver",
"text" => "Naver"
@@ -329,10 +317,6 @@ $settings = [
"value" => "startpage",
"text" => "Startpage"
],
- [
- "value" => "qwant",
- "text" => "Qwant"
- ],
[
"value" => "naver",
"text" => "Naver"
@@ -379,10 +363,6 @@ $settings = [
"value" => "startpage",
"text" => "Startpage"
],
- [
- "value" => "qwant",
- "text" => "Qwant"
- ],
/*[
"value" => "mojeek",
"text" => "Mojeek"
diff --git a/static/themes/Degoog dark.css b/static/themes/Degoog dark.css
new file mode 100644
index 0000000..96c7cab
--- /dev/null
+++ b/static/themes/Degoog dark.css
@@ -0,0 +1,1495 @@
+@charset "UTF-8";
+
+:root{
+ --warning: #fbbc05;
+ --bg: #25272e;
+ --bg-light: #303134;
+ --bg-hover: #3c4043;
+ --border: #5f6368;
+ --border-light: #303134;
+ --text-primary: #dddddd;
+ --text-secondary: #9aa0a6;
+ --text-link: #8ab4f8;
+ --text-link-visited: #c58af9;
+ --text-cite: #bdc1c6;
+ --text-snippet: #bdc1c6;
+ --search-bar-bg: #303134;
+ --search-bar-bg-hover: #303134;
+ --btn-bg: #303134;
+ --overlay-bg: rgba(0, 0, 0, 0.6);
+ --white: #fff3f3;
+}
+
+:root {
+ --gutter: clamp(
+ 1rem,
+ calc(1rem + max(0px, (100vw - 48rem) * 0.31)),
+ 9.375rem
+ );
+ --main-width: 40.75rem;
+ --sidebar-width: 21.25rem;
+ --column-gap: 2.5rem;
+ --bar-height: 2.75rem;
+ --bar-radius: 1.5rem;
+ --radius-xs: 0.1875rem;
+ --radius-sm: 0.25rem;
+ --radius-md: 0.375rem;
+ --radius-lg: 0.5rem;
+ --shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ --fast: 0.2s ease;
+ --icon-search: url("data:image/svg+xml;utf8,");
+ --icon-chevron: url("data:image/svg+xml;utf8,");
+
+ --1d2021: var(--bg);
+ --282828: var(--bg-light);
+ --3c3836: var(--bg-hover);
+ --504945: var(--border-light);
+ --928374: var(--text-secondary);
+ --a89984: var(--text-snippet);
+ --bdae93: var(--text-primary);
+ --8ec07c: var(--success);
+ --ebdbb2: var(--text-primary);
+ --comment: var(--text-secondary);
+ --default: var(--text-primary);
+ --keyword: var(--warning);
+ --string: var(--text-link);
+ --green: var(--success);
+ --yellow: var(--warning);
+ --red: var(--danger);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 100%;
+ font-family: sans-serif;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ max-width: none;
+ min-height: 100vh;
+ background: var(--bg);
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.875rem;
+ line-height: 1.5;
+ word-wrap: anywhere;
+}
+
+input,
+select,
+button,
+textarea {
+ font-family: var(--font);
+}
+
+a {
+ color: var(--text-link);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+::selection {
+ background: rgba(var(--primary-rgb), 0.3);
+}
+
+::-webkit-scrollbar {
+ width: 0.5rem;
+ height: 0.5rem;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: var(--radius-sm);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: var(--text-primary);
+ font-weight: 500;
+ line-height: 1.3;
+ margin: 0 0 0.75rem;
+}
+
+h1 {
+ font-size: 1.75rem;
+}
+
+h2 {
+ font-size: 1.25rem;
+}
+
+h3 {
+ font-size: 1.125rem;
+}
+
+h4,
+h5,
+h6 {
+ font-size: 1rem;
+}
+
+b,
+strong {
+ font-weight: 600;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid var(--border-light);
+ margin: 1.5rem 0;
+}
+
+ul,
+ol {
+ padding-left: 1.25rem;
+ margin: 0.5rem 0;
+}
+
+.quote {
+ font-style: normal;
+ color: var(--text-secondary);
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ margin: 1rem 0;
+ padding: 0.75rem 1rem;
+}
+
+.navigation {
+ position: absolute;
+ top: 1.15rem;
+ right: var(--gutter);
+ left: auto;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ z-index: 2;
+}
+
+.navigation a {
+ color: var(--text-secondary);
+ text-decoration: none;
+}
+
+.navigation a:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.navigation a:not(:last-child)::after {
+ content: none;
+}
+
+.logo a {
+ display: inline-block;
+ text-decoration: none;
+}
+
+.home .logo {
+ width: auto;
+ max-width: none;
+ height: auto;
+ margin: 0 0 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.home .logo img {
+ display: block;
+ width: auto;
+ max-width: min(100%, 22rem);
+ height: auto;
+}
+
+body.home {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 2rem 1rem 4rem;
+ text-align: center;
+}
+
+.home #center {
+ display: block;
+ width: min(36.5rem, 100%);
+ vertical-align: baseline;
+}
+
+.home form {
+ display: block;
+ margin-bottom: 1.75rem;
+}
+
+.home .searchbox {
+ width: 100%;
+ margin: 0 auto;
+ text-align: left;
+}
+
+.home #center > a {
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+.home #center > a:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.home .subtext {
+ margin-top: 1.25rem;
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ line-height: 1.6;
+}
+
+.home .subtext a {
+ color: var(--text-secondary);
+}
+
+.searchbox {
+ display: flex;
+ align-items: center;
+ position: relative;
+ width: 100%;
+ max-width: 43.25rem;
+ height: var(--bar-height);
+ margin: 0;
+ padding: 0 0.75rem;
+ background: var(--search-bar-bg);
+ border: 0;
+ border-radius: var(--bar-radius);
+ box-shadow: var(--shadow);
+ transition:
+ background var(--fast),
+ box-shadow var(--fast);
+}
+
+.searchbox::before {
+ content: "";
+ flex-shrink: 0;
+ width: 1.125rem;
+ height: 1.125rem;
+ margin-right: 0.75rem;
+ background-color: var(--text-secondary);
+ -webkit-mask: var(--icon-search) center/contain no-repeat;
+ mask: var(--icon-search) center/contain no-repeat;
+}
+
+.searchbox:hover,
+.searchbox:focus-within {
+ background: var(--search-bar-bg-hover);
+ border: 0;
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.16);
+}
+
+.searchbox .wrapper {
+ order: 1;
+ flex: 1;
+ min-width: 0;
+ overflow: visible;
+}
+
+.searchbox input {
+ all: unset;
+ box-sizing: border-box;
+ height: var(--bar-height);
+ line-height: var(--bar-height);
+ color: var(--text-primary);
+ font-family: var(--font);
+}
+
+.searchbox input[type="text"] {
+ width: 100%;
+ padding-left: 0;
+ font-size: 1rem;
+}
+
+.searchbox input[type="text"]::placeholder {
+ color: var(--text-secondary);
+}
+
+.searchbox input[type="submit"] {
+ order: 2;
+ float: none;
+ flex-shrink: 0;
+ height: 2rem;
+ line-height: 2rem;
+ margin-left: 0.5rem;
+ padding: 0 0.75rem;
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background var(--fast),
+ color var(--fast);
+}
+
+.searchbox input[type="submit"]:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.searchbox:has(.autocomplete[style*="block"]) {
+ border-radius: var(--bar-radius) var(--bar-radius) 0 0;
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.16);
+}
+
+.autocomplete {
+ top: 100%;
+ left: 0;
+ right: 0;
+ padding: 0.25rem 0 0.5rem;
+ background: var(--search-bar-bg-hover);
+ border: 0;
+ border-radius: 0 0 var(--bar-radius) var(--bar-radius);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.16);
+ overflow: hidden;
+ z-index: 100;
+}
+
+.autocomplete .entry {
+ padding: 0.45rem 1rem 0.45rem 2.875rem;
+ color: var(--text-primary);
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.autocomplete .entry:hover,
+.autocomplete .entry:focus {
+ background: var(--bg-hover);
+}
+
+body > form {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ align-items: center;
+ column-gap: 1.25rem;
+ padding: 1rem var(--gutter) 0;
+ margin: 0;
+}
+
+body > form .searchbox {
+ grid-column: 1;
+ grid-row: 1;
+}
+
+body > form .tabs {
+ grid-column: 1 / -1;
+ grid-row: 2;
+}
+
+body > form .filters {
+ grid-column: 1 / -1;
+ grid-row: 3;
+}
+
+.tabs {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ margin-top: 1rem;
+ padding: 0 0 0 0.25rem;
+ border-bottom: 1px solid var(--border-light);
+ overflow-x: auto;
+ scrollbar-width: none;
+}
+
+.tabs::-webkit-scrollbar {
+ display: none;
+}
+
+.tabs .tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ white-space: nowrap;
+ border-bottom: 3px solid transparent;
+ transition:
+ color var(--fast),
+ border-color var(--fast);
+}
+
+.tabs .tab:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.tabs .tab.selected {
+ color: var(--text-primary);
+ border-bottom: 3px solid var(--text-primary);
+}
+
+.filters {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0;
+ padding: 0.75rem 0 0;
+ overflow: visible;
+ white-space: normal;
+}
+
+.filters .filter {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0;
+ padding: 0.375rem 0.75rem;
+ background: var(--bg-light);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ vertical-align: middle;
+}
+
+.filters .filter .title {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ white-space: nowrap;
+}
+
+.filters .filter input,
+.filters .filter select {
+ all: unset;
+ display: block;
+ width: auto;
+ max-width: 11rem;
+ height: 1.5rem;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ line-height: 1.5rem;
+ cursor: pointer;
+}
+
+.filters .filter select {
+ padding-right: 1.125rem;
+ background-color: transparent;
+ background-image: var(--icon-chevron);
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: 0.875rem;
+}
+
+.timetaken {
+ margin: 0;
+ padding: 1.25rem var(--gutter) 0.75rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 400;
+}
+
+#overflow {
+ overflow: visible;
+}
+
+#overflow.web {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--column-gap);
+ padding: 0 var(--gutter) 3rem;
+}
+
+body > #overflow:first-child {
+ padding-top: 2rem;
+}
+
+.web .left {
+ float: none;
+ order: 1;
+ flex: 1;
+ min-width: 0;
+ width: auto;
+ max-width: var(--main-width);
+}
+
+.web .right-wrapper {
+ float: none;
+ order: 2;
+ flex-shrink: 0;
+ width: var(--sidebar-width);
+ padding-left: 0;
+ overflow: visible;
+}
+
+.web .right-wrapper:not(:has(> div:not(:empty))) {
+ display: none;
+}
+
+.web .right-right,
+.web .right-left {
+ float: none;
+ width: 100%;
+ padding: 0;
+ overflow: visible;
+}
+
+.web .text-result {
+ margin: 0 0 2rem;
+ padding: 0;
+}
+
+.web .url {
+ display: flow-root;
+ position: relative;
+ line-height: 1.625rem;
+}
+
+.favicon {
+ all: unset;
+ float: left;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 1.625rem;
+ height: 1.625rem;
+ margin-right: 0.625rem;
+ border-radius: 50%;
+ background: var(--bg-hover);
+ cursor: pointer;
+}
+
+.web .favicon img {
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ margin: 0;
+ object-fit: contain;
+ font-size: 0.625rem;
+ line-height: 1;
+ text-align: center;
+}
+
+.web .url .part {
+ color: var(--text-cite);
+ font-size: 0.875rem;
+}
+
+.web .url .part:hover {
+ text-decoration: none;
+}
+
+.web .separator::before {
+ content: "›";
+ padding: 0 0.375rem;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+.favicon-dropdown {
+ top: 2rem;
+ left: 0;
+ padding: 0.25rem 0;
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ z-index: 20;
+}
+
+.favicon-dropdown::before {
+ content: none;
+}
+
+.favicon-dropdown a {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0.875rem;
+ color: var(--text-primary);
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.favicon-dropdown a:hover {
+ background: var(--bg-hover);
+ text-decoration: none;
+}
+
+.favicon-dropdown img {
+ float: none;
+ width: 1rem;
+ height: 1rem;
+ margin: 0;
+ flex-shrink: 0;
+}
+
+.web .hover {
+ display: flow-root;
+ clear: none;
+ padding-top: 0;
+ color: var(--text-snippet);
+}
+
+.web .text-result .title {
+ margin: 0.125rem 0 0;
+ color: var(--text-link);
+ font-size: 1.25rem;
+ font-weight: 400;
+ line-height: 1.3;
+}
+
+.web .text-result a:visited .title {
+ color: var(--text-link-visited) !important;
+}
+
+.web .text-result .hover:hover .title {
+ text-decoration: underline;
+}
+
+.web .description {
+ margin-top: 0.25rem;
+ color: var(--text-snippet);
+ font-size: 0.875rem;
+ line-height: 1.58;
+ white-space: pre-line;
+}
+
+.web .type {
+ float: left;
+ margin: 0.3rem 0.5rem 0 0;
+ padding: 0 0.375rem;
+ background: var(--bg-hover);
+ border: 0;
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ font-weight: 500;
+ line-height: 1.4;
+}
+
+.web .text-result .greentext {
+ margin-top: 0.25rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+.web .text-result .author {
+ font-style: normal;
+ color: var(--text-secondary);
+}
+
+.thumb-wrap {
+ float: right;
+ width: 7.5rem;
+ height: 5.625rem;
+ margin: 0.25rem 0 0.5rem 0.75rem;
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+}
+
+.thumb-wrap.portrait {
+ width: 4.25rem;
+}
+
+.thumb-wrap.square {
+ width: 5.625rem;
+}
+
+.thumb-wrap .thumb {
+ object-fit: cover;
+}
+
+.duration {
+ right: 0.375rem;
+ bottom: 0.375rem;
+ padding: 1px 0.25rem;
+ background: rgba(0, 0, 0, 0.75);
+ border: 0;
+ border-radius: var(--radius-xs);
+ color: #fff;
+ font-family: var(--font);
+ font-size: 0.6875rem;
+ line-height: 1.3;
+}
+
+.web .sublinks {
+ padding: 0.75rem 0 0;
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+}
+
+.web .sublinks table td {
+ padding-bottom: 0.75rem;
+}
+
+.web .sublinks .title {
+ color: var(--text-link);
+ font-size: 0.875rem;
+}
+
+.web .sublinks .description {
+ font-size: 0.8125rem;
+ line-height: 1.5;
+}
+
+.infobox,
+.web .answer-wrapper,
+.web .info-table,
+.web .wiki-head table,
+.about table,
+.related,
+.web .settings,
+.code,
+.nextpage,
+.web .spoiler-button {
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+}
+
+.infobox {
+ margin-bottom: 1rem;
+ padding: 1rem;
+ color: var(--text-primary);
+ overflow: hidden;
+}
+
+.infobox h1 {
+ font-size: 1.25rem;
+}
+
+.infobox h2 {
+ font-size: 1.125rem;
+}
+
+.infobox a {
+ color: var(--text-link);
+}
+
+.infobox ul {
+ padding-left: 1.25rem;
+ margin-bottom: 0;
+}
+
+.web .answer-wrapper {
+ margin-bottom: 1rem;
+ padding: 1rem;
+ overflow: hidden;
+}
+
+.web .answer {
+ padding-bottom: 0;
+ max-height: 34rem;
+}
+
+.web .answer::after {
+ height: 2.5rem;
+ background: linear-gradient(transparent, var(--bg-light));
+}
+
+.web .answer-title {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.web .answer-title h1 {
+ font-size: 1.375rem;
+ margin-bottom: 0.5rem;
+}
+
+.web .spoiler-button {
+ margin-top: 0.75rem;
+ padding: 0 0.75rem;
+ line-height: 2.25rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ box-shadow: none;
+ background: var(--bg-hover);
+}
+
+.web .spoiler-button:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.web .wiki-head .photo {
+ margin: 0 0 0.75rem 0.75rem;
+}
+
+.web .wiki-head .photo img {
+ max-width: 9rem;
+ max-height: 9rem;
+ border-radius: var(--radius-lg);
+ filter: none;
+}
+
+.web .wiki-head .description {
+ padding-top: 0.5rem;
+ color: var(--text-snippet);
+ line-height: 1.58;
+}
+
+.web .wiki-head h2 {
+ font-size: 1.125rem;
+ margin: 1.25rem 0 0.5rem;
+}
+
+.web .wiki-head h2:first-child {
+ margin-top: 0.5rem;
+}
+
+.web .wiki-head a {
+ color: var(--text-link);
+}
+
+.web .wiki-head .socials {
+ margin-top: 1rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ overflow: visible;
+}
+
+.web .wiki-head .socials a {
+ float: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 4.25rem;
+ height: auto;
+ padding: 0.625rem 0.25rem;
+ border-radius: var(--radius-lg);
+ color: var(--text-secondary);
+ transition: background var(--fast);
+}
+
+.web .wiki-head .socials a:hover {
+ background: var(--bg-hover);
+}
+
+.web .wiki-head .socials a:hover .title {
+ text-decoration: none;
+ color: var(--text-primary);
+}
+
+.web .wiki-head .socials img {
+ width: 1.5rem;
+ height: 1.5rem;
+ line-height: 1.5rem;
+}
+
+.web .wiki-head .socials .title {
+ margin-top: 0.5rem;
+ font-size: 0.6875rem;
+ line-height: 1.3;
+}
+
+.web .fullimg {
+ margin: 0.5rem 0 1rem;
+ border: 0;
+ border-radius: var(--radius-lg);
+ max-height: 11rem;
+}
+
+table {
+ width: 100%;
+ text-align: left;
+ border-collapse: collapse;
+}
+
+table a {
+ color: var(--text-link);
+}
+
+.web .info-table,
+.web .wiki-head table,
+.about table {
+ margin: 0.75rem 0 0;
+ overflow: hidden;
+ color: var(--text-snippet);
+ font-size: 0.8125rem;
+}
+
+.web .info-table td,
+.web .wiki-head td,
+.about table td {
+ padding: 0.5rem 0.875rem;
+ vertical-align: top;
+}
+
+.web .info-table tr td:first-child,
+.web .wiki-head tr td:first-child,
+.about table tr td:first-child {
+ width: 34%;
+ min-width: 0;
+ padding-right: 0.875rem;
+ color: var(--text-secondary);
+ white-space: normal;
+}
+
+.web .info-table tr:nth-child(even),
+.web .wiki-head tr:nth-child(odd),
+.about table tr:nth-child(odd) {
+ background: var(--bg-hover);
+}
+
+.related {
+ margin: 0 0 2rem;
+ padding: 0.25rem 0;
+ overflow: hidden;
+}
+
+.related td {
+ padding: 0;
+ vertical-align: middle;
+}
+
+.related tr td:first-child {
+ padding-right: 0;
+}
+
+.related a {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.6rem 0.875rem;
+ color: var(--text-primary);
+ font-size: 0.875rem;
+ line-height: 1.4;
+}
+
+.related a::before {
+ content: "";
+ flex-shrink: 0;
+ width: 1rem;
+ height: 1rem;
+ background-color: var(--text-secondary);
+ -webkit-mask: var(--icon-search) center/contain no-repeat;
+ mask: var(--icon-search) center/contain no-repeat;
+}
+
+.related a:hover {
+ background: var(--bg-hover);
+ text-decoration: none;
+}
+
+.nextpage {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: auto;
+ margin: 0 0 2rem;
+ padding: 0 1.125rem;
+ min-height: 2.25rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ text-align: left;
+ transition:
+ background var(--fast),
+ color var(--fast);
+}
+
+.nextpage:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.code {
+ margin: 0.5rem 0 0.875rem;
+ padding: 0.75rem 0.875rem;
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ font-family: var(--mono);
+ font-size: 0.8125rem;
+ box-shadow: none;
+ overflow-x: auto;
+}
+
+.code-inline {
+ display: inline;
+ padding: 0.05rem 0.375rem;
+ background: var(--bg-hover);
+ border: 0;
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-family: var(--mono);
+ font-size: 0.8125rem;
+}
+
+.web .images {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ margin: 0.5rem 0 0;
+ overflow: visible;
+ font-size: 0;
+}
+
+.web .images .image {
+ width: 5.5rem;
+ height: 5.5rem;
+ padding: 0;
+ line-height: 0;
+ border-radius: var(--radius-lg);
+ background: var(--bg-hover);
+ overflow: hidden;
+ transition: opacity var(--fast);
+}
+
+.web .images .image:hover {
+ opacity: 0.85;
+}
+
+.web .images .image img {
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ max-height: none;
+ object-fit: cover;
+ vertical-align: top;
+}
+
+.web .images .duration {
+ border: 0;
+ right: 0.375rem;
+ bottom: 0.375rem;
+}
+
+#images {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
+ gap: 1rem 0.75rem;
+ margin: 0;
+ padding: 0.5rem var(--gutter) 1.5rem;
+ overflow: visible;
+}
+
+#images .infobox {
+ grid-column: 1 / -1;
+ width: auto;
+ max-width: var(--main-width);
+ margin-bottom: 0.5rem;
+}
+
+#images .image-wrapper {
+ float: none;
+ width: auto;
+ line-height: 1.4;
+}
+
+#images .image {
+ width: auto;
+ max-width: none;
+ margin: 0;
+ padding: 0;
+ font-size: 0.8125rem;
+}
+
+#images .thumb {
+ height: 11.25rem;
+ margin-bottom: 0.5rem;
+ border-radius: var(--radius-lg);
+ background: var(--bg-hover);
+ overflow: hidden;
+ transition: opacity var(--fast);
+}
+
+#images .image:hover .thumb {
+ opacity: 0.85;
+}
+
+#images img {
+ object-fit: cover;
+}
+
+#images .duration {
+ border: 0;
+}
+
+#images .image .title,
+#images .image .i-title-wrapper {
+ margin-bottom: 0.125rem;
+ color: var(--text-primary);
+ font-size: 0.8125rem;
+ font-weight: 400;
+ text-overflow: ellipsis;
+}
+
+#images .image .description {
+ height: auto;
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.nextpage.img {
+ width: auto;
+ margin: 0 var(--gutter) 3rem;
+}
+
+#popup-bg {
+ background: #000;
+ opacity: 0.92;
+}
+
+#popup-image {
+ background: transparent;
+ border: 0;
+}
+
+#popup-status {
+ height: 2.75rem;
+ background: var(--bg-light);
+ border: 0;
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
+}
+
+#popup-num,
+#popup-title {
+ padding: 0 0.875rem;
+ line-height: 2.75rem;
+ color: var(--text-primary);
+ font-size: 0.8125rem;
+}
+
+#popup-num {
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+#popup-status select {
+ all: unset;
+ display: block;
+ width: 15rem;
+ height: 2rem;
+ margin-right: 0.75rem;
+ padding: 0 1.75rem 0 0.75rem;
+ background-color: var(--bg-hover);
+ background-image: var(--icon-chevron);
+ background-repeat: no-repeat;
+ background-position: right 0.625rem center;
+ background-size: 0.875rem;
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.8125rem;
+ line-height: 2rem;
+ cursor: pointer;
+}
+
+.captcha-wrapper {
+ border: 0;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.captcha-wrapper img {
+ background: var(--bg-light);
+}
+
+.captcha-wrapper input:checked + label::after {
+ border-right-color: var(--white);
+ border-bottom-color: var(--white);
+}
+
+.web .settings {
+ margin-top: 1rem;
+ padding: 1rem 1rem 0.25rem;
+}
+
+.web .settings h2 {
+ font-size: 1.125rem;
+ margin-bottom: 1rem;
+}
+
+.web .setting {
+ margin-bottom: 1.25rem;
+}
+
+.web .setting .title {
+ margin-bottom: 0.375rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ line-height: 1.5;
+}
+
+.web .setting .title i {
+ font-style: normal;
+ color: var(--text-secondary);
+}
+
+.web .settings select {
+ all: unset;
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 22rem;
+ height: 2.25rem;
+ padding: 0 2rem 0 0.75rem;
+ background-color: var(--bg);
+ background-image: var(--icon-chevron);
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 0.875rem;
+ border: 0;
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.875rem;
+ line-height: 2.25rem;
+ cursor: pointer;
+}
+
+.web .settings-submit {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin: 1.25rem 0 0;
+}
+
+.web .settings-submit input {
+ all: unset;
+ order: 1;
+ height: 2.25rem;
+ padding: 0 1.25rem;
+ background: var(--primary);
+ border-radius: var(--radius-sm);
+ box-shadow: var(--shadow);
+ color: var(--white);
+ font-family: var(--font);
+ font-size: 0.875rem;
+ font-weight: 500;
+ line-height: 2.25rem;
+ cursor: pointer;
+ transition: background var(--fast);
+}
+
+.web .settings-submit input:hover {
+ background: var(--primary-hover);
+}
+
+.web .settings-submit a {
+ order: 2;
+ margin-right: 0;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+body.about {
+ margin: 0;
+ padding: 2rem var(--gutter) 4rem;
+ max-width: calc(var(--main-width) + var(--gutter) * 2);
+}
+
+.about a {
+ color: var(--text-link);
+}
+
+.about h1,
+.about h2 {
+ margin-top: 1.75rem;
+}
+
+.about table {
+ margin-bottom: 1rem;
+}
+
+body.instances {
+ margin: 0;
+ padding: 2rem var(--gutter) 4rem;
+}
+
+.instances a {
+ color: var(--text-link);
+}
+
+.instances table {
+ margin-top: 1rem;
+ background: var(--bg-light);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+}
+
+.instances thead {
+ outline: 0;
+ background: var(--bg-hover);
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+.instances tbody {
+ outline: 0;
+ top: 0;
+}
+
+.instances th,
+.instances td {
+ padding: 0.5rem 0.875rem;
+}
+
+.instances th:hover {
+ background: var(--border-light);
+}
+
+.instances tbody tr:nth-child(even) {
+ background: var(--bg-hover);
+}
+
+.instances tbody tr:hover {
+ background: var(--border-light);
+}
+
+.instances .arrow {
+ border-top-color: var(--text-secondary);
+}
+
+.instances .arrow.up {
+ border-top-color: transparent;
+ border-bottom-color: var(--text-secondary);
+}
+
+.instances .popup {
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+ padding: 1rem 1.25rem;
+}
+
+.instances .go-back {
+ margin-top: 1rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+@media only screen and (max-width: 1550px) {
+ .web .right-right,
+ .web .right-left {
+ padding: 0;
+ }
+
+ .web .left,
+ .searchbox,
+ #images .infobox {
+ width: auto;
+ }
+
+ .web .right-wrapper {
+ width: var(--sidebar-width);
+ }
+}
+
+@media only screen and (max-width: 1000px) {
+ #overflow.web {
+ flex-direction: column;
+ gap: 0;
+ }
+
+ .web .left,
+ .web .right-wrapper {
+ width: 100%;
+ max-width: none;
+ }
+
+ .web .right-wrapper {
+ order: 0;
+ margin-bottom: 1.5rem;
+ }
+
+ .web.has-answer .left::before {
+ display: none;
+ }
+
+ body:not(.instances) table td {
+ display: table-cell;
+ width: auto;
+ }
+
+ .related td {
+ display: block;
+ width: 100%;
+ }
+
+ .web .info-table tr td:first-child,
+ .web .wiki-head tr td:first-child {
+ text-decoration: none;
+ }
+}
+
+@media only screen and (max-width: 767px) {
+ .navigation {
+ position: static;
+ justify-content: flex-end;
+ padding: 1rem var(--gutter) 0;
+ }
+
+ body > form {
+ grid-template-columns: minmax(0, 1fr);
+ padding-top: 0.5rem;
+ }
+
+ body > form .searchbox {
+ grid-column: 1;
+ grid-row: 1;
+ max-width: none;
+ }
+
+ body > form .tabs {
+ grid-row: 2;
+ }
+
+ body > form .filters {
+ display: flex;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ padding-bottom: 0.5rem;
+ scrollbar-width: thin;
+ }
+
+ .web .answer {
+ max-height: 16rem;
+ }
+
+ .thumb-wrap {
+ width: 5rem;
+ height: 3.75rem;
+ }
+}
diff --git a/static/themes/Degoog light.css b/static/themes/Degoog light.css
new file mode 100644
index 0000000..1861e67
--- /dev/null
+++ b/static/themes/Degoog light.css
@@ -0,0 +1,1501 @@
+@charset "UTF-8";
+
+:root {
+ --primary: #5673ac;
+ --primary-hover: #3b547c;
+ --primary-rgb: 66, 133, 244;
+ --danger: #ea4335;
+ --warning: #c17d00;
+ --success: #34a853;
+
+ --bg: #f1f1f1;
+ --bg-light: #fafafa;
+ --bg-hover: #fafafa;
+ --border: #c8d0da;
+ --border-light: #dedede;
+ --text-primary: #343536;
+ --text-secondary: #4b5563;
+ --text-link: #1a0dab;
+ --text-link-visited: #681da8;
+ --text-cite: #111827;
+ --text-snippet: #374151;
+ --search-bar-bg: #f7f8fb;
+ --search-bar-bg-hover: #f7f8fb;
+ --btn-bg: #f7f8fb;
+ --overlay-bg: rgba(0, 0, 0, 0.4);
+ --white: #fff;
+}
+
+:root {
+ --gutter: clamp(
+ 1rem,
+ calc(1rem + max(0px, (100vw - 48rem) * 0.31)),
+ 9.375rem
+ );
+ --main-width: 40.75rem;
+ --sidebar-width: 21.25rem;
+ --column-gap: 2.5rem;
+ --bar-height: 2.75rem;
+ --bar-radius: 1.5rem;
+ --radius-xs: 0.1875rem;
+ --radius-sm: 0.25rem;
+ --radius-md: 0.375rem;
+ --radius-lg: 0.5rem;
+ --shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ --fast: 0.2s ease;
+ --icon-search: url("data:image/svg+xml;utf8,");
+ --icon-chevron: url("data:image/svg+xml;utf8,");
+
+ --1d2021: var(--bg);
+ --282828: var(--bg-light);
+ --3c3836: var(--bg-hover);
+ --504945: var(--border-light);
+ --928374: var(--text-secondary);
+ --a89984: var(--text-snippet);
+ --bdae93: var(--text-primary);
+ --8ec07c: var(--success);
+ --ebdbb2: var(--text-primary);
+ --comment: var(--text-secondary);
+ --default: var(--text-primary);
+ --keyword: var(--warning);
+ --string: var(--text-link);
+ --green: var(--success);
+ --yellow: var(--warning);
+ --red: var(--danger);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 100%;
+ font-family: sans-serif;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ max-width: none;
+ min-height: 100vh;
+ background: var(--bg);
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.875rem;
+ line-height: 1.5;
+ word-wrap: anywhere;
+}
+
+input,
+select,
+button,
+textarea {
+ font-family: var(--font);
+}
+
+a {
+ color: var(--text-link);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+::selection {
+ background: rgba(var(--primary-rgb), 0.3);
+}
+
+::-webkit-scrollbar {
+ width: 0.5rem;
+ height: 0.5rem;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: var(--radius-sm);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ color: var(--text-primary);
+ font-weight: 500;
+ line-height: 1.3;
+ margin: 0 0 0.75rem;
+}
+
+h1 {
+ font-size: 1.75rem;
+}
+
+h2 {
+ font-size: 1.25rem;
+}
+
+h3 {
+ font-size: 1.125rem;
+}
+
+h4,
+h5,
+h6 {
+ font-size: 1rem;
+}
+
+b,
+strong {
+ font-weight: 600;
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid var(--border-light);
+ margin: 1.5rem 0;
+}
+
+ul,
+ol {
+ padding-left: 1.25rem;
+ margin: 0.5rem 0;
+}
+
+.quote {
+ font-style: normal;
+ color: var(--text-secondary);
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ margin: 1rem 0;
+ padding: 0.75rem 1rem;
+}
+
+.navigation {
+ position: absolute;
+ top: 1.15rem;
+ right: var(--gutter);
+ left: auto;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ z-index: 2;
+}
+
+.navigation a {
+ color: var(--text-secondary);
+ text-decoration: none;
+}
+
+.navigation a:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.navigation a:not(:last-child)::after {
+ content: none;
+}
+
+.logo a {
+ display: inline-block;
+ text-decoration: none;
+}
+
+.home .logo {
+ width: auto;
+ max-width: none;
+ height: auto;
+ margin: 0 0 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.home .logo img {
+ display: block;
+ width: auto;
+ max-width: min(100%, 22rem);
+ height: auto;
+}
+
+body.home {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 2rem 1rem 4rem;
+ text-align: center;
+}
+
+.home #center {
+ display: block;
+ width: min(36.5rem, 100%);
+ vertical-align: baseline;
+}
+
+.home form {
+ display: block;
+ margin-bottom: 1.75rem;
+}
+
+.home .searchbox {
+ width: 100%;
+ margin: 0 auto;
+ text-align: left;
+}
+
+.home #center > a {
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+.home #center > a:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.home .subtext {
+ margin-top: 1.25rem;
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ line-height: 1.6;
+}
+
+.home .subtext a {
+ color: var(--text-secondary);
+}
+
+.searchbox {
+ display: flex;
+ align-items: center;
+ position: relative;
+ width: 100%;
+ max-width: 43.25rem;
+ height: var(--bar-height);
+ margin: 0;
+ padding: 0 0.75rem;
+ background: var(--search-bar-bg);
+ border: 0;
+ border-radius: var(--bar-radius);
+ box-shadow: var(--shadow);
+ transition:
+ background var(--fast),
+ box-shadow var(--fast);
+}
+
+.searchbox::before {
+ content: "";
+ flex-shrink: 0;
+ width: 1.125rem;
+ height: 1.125rem;
+ margin-right: 0.75rem;
+ background-color: var(--text-secondary);
+ -webkit-mask: var(--icon-search) center/contain no-repeat;
+ mask: var(--icon-search) center/contain no-repeat;
+}
+
+.searchbox:hover,
+.searchbox:focus-within {
+ background: var(--search-bar-bg-hover);
+ border: 0;
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.16);
+}
+
+.searchbox .wrapper {
+ order: 1;
+ flex: 1;
+ min-width: 0;
+ overflow: visible;
+}
+
+.searchbox input {
+ all: unset;
+ box-sizing: border-box;
+ height: var(--bar-height);
+ line-height: var(--bar-height);
+ color: var(--text-primary);
+ font-family: var(--font);
+}
+
+.searchbox input[type="text"] {
+ width: 100%;
+ padding-left: 0;
+ font-size: 1rem;
+}
+
+.searchbox input[type="text"]::placeholder {
+ color: var(--text-secondary);
+}
+
+.searchbox input[type="submit"] {
+ order: 2;
+ float: none;
+ flex-shrink: 0;
+ height: 2rem;
+ line-height: 2rem;
+ margin-left: 0.5rem;
+ padding: 0 0.75rem;
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background var(--fast),
+ color var(--fast);
+}
+
+.searchbox input[type="submit"]:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.searchbox:has(.autocomplete[style*="block"]) {
+ border-radius: var(--bar-radius) var(--bar-radius) 0 0;
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.16);
+}
+
+.autocomplete {
+ top: 100%;
+ left: 0;
+ right: 0;
+ padding: 0.25rem 0 0.5rem;
+ background: var(--search-bar-bg-hover);
+ border: 0;
+ border-radius: 0 0 var(--bar-radius) var(--bar-radius);
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.16);
+ overflow: hidden;
+ z-index: 100;
+}
+
+.autocomplete .entry {
+ padding: 0.45rem 1rem 0.45rem 2.875rem;
+ color: var(--text-primary);
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.autocomplete .entry:hover,
+.autocomplete .entry:focus {
+ background: var(--bg-hover);
+}
+
+body > form {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ align-items: center;
+ column-gap: 1.25rem;
+ padding: 1rem var(--gutter) 0;
+ margin: 0;
+}
+
+body > form .searchbox {
+ grid-column: 1;
+ grid-row: 1;
+}
+
+body > form .tabs {
+ grid-column: 1 / -1;
+ grid-row: 2;
+}
+
+body > form .filters {
+ grid-column: 1 / -1;
+ grid-row: 3;
+}
+
+.tabs {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ margin-top: 1rem;
+ padding: 0 0 0 0.25rem;
+ border-bottom: 1px solid var(--border-light);
+ overflow-x: auto;
+ scrollbar-width: none;
+}
+
+.tabs::-webkit-scrollbar {
+ display: none;
+}
+
+.tabs .tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ white-space: nowrap;
+ border-bottom: 3px solid transparent;
+ transition:
+ color var(--fast),
+ border-color var(--fast);
+}
+
+.tabs .tab:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.tabs .tab.selected {
+ color: var(--text-primary);
+ border-bottom: 3px solid var(--text-primary);
+}
+
+.filters {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0;
+ padding: 0.75rem 0 0;
+ overflow: visible;
+ white-space: normal;
+}
+
+.filters .filter {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0;
+ padding: 0.375rem 0.75rem;
+ background: var(--bg-light);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ vertical-align: middle;
+}
+
+.filters .filter .title {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ white-space: nowrap;
+}
+
+.filters .filter input,
+.filters .filter select {
+ all: unset;
+ display: block;
+ width: auto;
+ max-width: 11rem;
+ height: 1.5rem;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ line-height: 1.5rem;
+ cursor: pointer;
+}
+
+.filters .filter select {
+ padding-right: 1.125rem;
+ background-color: transparent;
+ background-image: var(--icon-chevron);
+ background-repeat: no-repeat;
+ background-position: right center;
+ background-size: 0.875rem;
+}
+
+.timetaken {
+ margin: 0;
+ padding: 1.25rem var(--gutter) 0.75rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 400;
+}
+
+#overflow {
+ overflow: visible;
+}
+
+#overflow.web {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--column-gap);
+ padding: 0 var(--gutter) 3rem;
+}
+
+body > #overflow:first-child {
+ padding-top: 2rem;
+}
+
+.web .left {
+ float: none;
+ order: 1;
+ flex: 1;
+ min-width: 0;
+ width: auto;
+ max-width: var(--main-width);
+}
+
+.web .right-wrapper {
+ float: none;
+ order: 2;
+ flex-shrink: 0;
+ width: var(--sidebar-width);
+ padding-left: 0;
+ overflow: visible;
+}
+
+.web .right-wrapper:not(:has(> div:not(:empty))) {
+ display: none;
+}
+
+.web .right-right,
+.web .right-left {
+ float: none;
+ width: 100%;
+ padding: 0;
+ overflow: visible;
+}
+
+.web .text-result {
+ margin: 0 0 2rem;
+ padding: 0;
+}
+
+.web .url {
+ display: flow-root;
+ position: relative;
+ line-height: 1.625rem;
+}
+
+.favicon {
+ all: unset;
+ float: left;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 1.625rem;
+ height: 1.625rem;
+ margin-right: 0.625rem;
+ border-radius: 50%;
+ background: var(--bg-hover);
+ cursor: pointer;
+}
+
+.web .favicon img {
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ margin: 0;
+ object-fit: contain;
+ font-size: 0.625rem;
+ line-height: 1;
+ text-align: center;
+}
+
+.web .url .part {
+ color: var(--text-cite);
+ font-size: 0.875rem;
+}
+
+.web .url .part:hover {
+ text-decoration: none;
+}
+
+.web .separator::before {
+ content: "›";
+ padding: 0 0.375rem;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+.favicon-dropdown {
+ top: 2rem;
+ left: 0;
+ padding: 0.25rem 0;
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ z-index: 20;
+}
+
+.favicon-dropdown::before {
+ content: none;
+}
+
+.favicon-dropdown a {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.4rem 0.875rem;
+ color: var(--text-primary);
+ font-size: 0.8125rem;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+.favicon-dropdown a:hover {
+ background: var(--bg-hover);
+ text-decoration: none;
+}
+
+.favicon-dropdown img {
+ float: none;
+ width: 1rem;
+ height: 1rem;
+ margin: 0;
+ flex-shrink: 0;
+}
+
+.web .hover {
+ display: flow-root;
+ clear: none;
+ padding-top: 0;
+ color: var(--text-snippet);
+}
+
+.web .text-result .title {
+ margin: 0.125rem 0 0;
+ color: var(--text-link);
+ font-size: 1.25rem;
+ font-weight: 400;
+ line-height: 1.3;
+}
+
+.web .text-result a:visited .title {
+ color: var(--text-link-visited) !important;
+}
+
+.web .text-result .hover:hover .title {
+ text-decoration: underline;
+}
+
+.web .description {
+ margin-top: 0.25rem;
+ color: var(--text-snippet);
+ font-size: 0.875rem;
+ line-height: 1.58;
+ white-space: pre-line;
+}
+
+.web .type {
+ float: left;
+ margin: 0.3rem 0.5rem 0 0;
+ padding: 0 0.375rem;
+ background: var(--bg-hover);
+ border: 0;
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ font-weight: 500;
+ line-height: 1.4;
+}
+
+.web .text-result .greentext {
+ margin-top: 0.25rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+.web .text-result .author {
+ font-style: normal;
+ color: var(--text-secondary);
+}
+
+.thumb-wrap {
+ float: right;
+ width: 7.5rem;
+ height: 5.625rem;
+ margin: 0.25rem 0 0.5rem 0.75rem;
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+}
+
+.thumb-wrap.portrait {
+ width: 4.25rem;
+}
+
+.thumb-wrap.square {
+ width: 5.625rem;
+}
+
+.thumb-wrap .thumb {
+ object-fit: cover;
+}
+
+.duration {
+ right: 0.375rem;
+ bottom: 0.375rem;
+ padding: 1px 0.25rem;
+ background: rgba(0, 0, 0, 0.75);
+ border: 0;
+ border-radius: var(--radius-xs);
+ color: #fff;
+ font-family: var(--font);
+ font-size: 0.6875rem;
+ line-height: 1.3;
+}
+
+.web .sublinks {
+ padding: 0.75rem 0 0;
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+}
+
+.web .sublinks table td {
+ padding-bottom: 0.75rem;
+}
+
+.web .sublinks .title {
+ color: var(--text-link);
+ font-size: 0.875rem;
+}
+
+.web .sublinks .description {
+ font-size: 0.8125rem;
+ line-height: 1.5;
+}
+
+.infobox,
+.web .answer-wrapper,
+.web .info-table,
+.web .wiki-head table,
+.about table,
+.related,
+.web .settings,
+.code,
+.nextpage,
+.web .spoiler-button {
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+}
+
+.infobox {
+ margin-bottom: 1rem;
+ padding: 1rem;
+ color: var(--text-primary);
+ overflow: hidden;
+}
+
+.infobox h1 {
+ font-size: 1.25rem;
+}
+
+.infobox h2 {
+ font-size: 1.125rem;
+}
+
+.infobox a {
+ color: var(--text-link);
+}
+
+.infobox ul {
+ padding-left: 1.25rem;
+ margin-bottom: 0;
+}
+
+.web .answer-wrapper {
+ margin-bottom: 1rem;
+ padding: 1rem;
+ overflow: hidden;
+}
+
+.web .answer {
+ padding-bottom: 0;
+ max-height: 34rem;
+}
+
+.web .answer::after {
+ height: 2.5rem;
+ background: linear-gradient(transparent, var(--bg-light));
+}
+
+.web .answer-title {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.web .answer-title h1 {
+ font-size: 1.375rem;
+ margin-bottom: 0.5rem;
+}
+
+.web .spoiler-button {
+ margin-top: 0.75rem;
+ padding: 0 0.75rem;
+ line-height: 2.25rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ box-shadow: none;
+ background: var(--bg-hover);
+}
+
+.web .spoiler-button:hover {
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.web .wiki-head .photo {
+ margin: 0 0 0.75rem 0.75rem;
+}
+
+.web .wiki-head .photo img {
+ max-width: 9rem;
+ max-height: 9rem;
+ border-radius: var(--radius-lg);
+ filter: none;
+}
+
+.web .wiki-head .description {
+ padding-top: 0.5rem;
+ color: var(--text-snippet);
+ line-height: 1.58;
+}
+
+.web .wiki-head h2 {
+ font-size: 1.125rem;
+ margin: 1.25rem 0 0.5rem;
+}
+
+.web .wiki-head h2:first-child {
+ margin-top: 0.5rem;
+}
+
+.web .wiki-head a {
+ color: var(--text-link);
+}
+
+.web .wiki-head .socials {
+ margin-top: 1rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ overflow: visible;
+}
+
+.web .wiki-head .socials a {
+ float: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 4.25rem;
+ height: auto;
+ padding: 0.625rem 0.25rem;
+ border-radius: var(--radius-lg);
+ color: var(--text-secondary);
+ transition: background var(--fast);
+}
+
+.web .wiki-head .socials a:hover {
+ background: var(--bg-hover);
+}
+
+.web .wiki-head .socials a:hover .title {
+ text-decoration: none;
+ color: var(--text-primary);
+}
+
+.web .wiki-head .socials img {
+ width: 1.5rem;
+ height: 1.5rem;
+ line-height: 1.5rem;
+}
+
+.web .wiki-head .socials .title {
+ margin-top: 0.5rem;
+ font-size: 0.6875rem;
+ line-height: 1.3;
+}
+
+.web .fullimg {
+ margin: 0.5rem 0 1rem;
+ border: 0;
+ border-radius: var(--radius-lg);
+ max-height: 11rem;
+}
+
+table {
+ width: 100%;
+ text-align: left;
+ border-collapse: collapse;
+}
+
+table a {
+ color: var(--text-link);
+}
+
+.web .info-table,
+.web .wiki-head table,
+.about table {
+ margin: 0.75rem 0 0;
+ overflow: hidden;
+ color: var(--text-snippet);
+ font-size: 0.8125rem;
+}
+
+.web .info-table td,
+.web .wiki-head td,
+.about table td {
+ padding: 0.5rem 0.875rem;
+ vertical-align: top;
+}
+
+.web .info-table tr td:first-child,
+.web .wiki-head tr td:first-child,
+.about table tr td:first-child {
+ width: 34%;
+ min-width: 0;
+ padding-right: 0.875rem;
+ color: var(--text-secondary);
+ white-space: normal;
+}
+
+.web .info-table tr:nth-child(even),
+.web .wiki-head tr:nth-child(odd),
+.about table tr:nth-child(odd) {
+ background: var(--bg-hover);
+}
+
+.related {
+ margin: 0 0 2rem;
+ padding: 0.25rem 0;
+ overflow: hidden;
+}
+
+.related td {
+ padding: 0;
+ vertical-align: middle;
+}
+
+.related tr td:first-child {
+ padding-right: 0;
+}
+
+.related a {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.6rem 0.875rem;
+ color: var(--text-primary);
+ font-size: 0.875rem;
+ line-height: 1.4;
+}
+
+.related a::before {
+ content: "";
+ flex-shrink: 0;
+ width: 1rem;
+ height: 1rem;
+ background-color: var(--text-secondary);
+ -webkit-mask: var(--icon-search) center/contain no-repeat;
+ mask: var(--icon-search) center/contain no-repeat;
+}
+
+.related a:hover {
+ background: var(--bg-hover);
+ text-decoration: none;
+}
+
+.nextpage {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: auto;
+ margin: 0 0 2rem;
+ padding: 0 1.125rem;
+ min-height: 2.25rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ text-align: left;
+ transition:
+ background var(--fast),
+ color var(--fast);
+}
+
+.nextpage:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.code {
+ margin: 0.5rem 0 0.875rem;
+ padding: 0.75rem 0.875rem;
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ font-family: var(--mono);
+ font-size: 0.8125rem;
+ box-shadow: none;
+ overflow-x: auto;
+}
+
+.code-inline {
+ display: inline;
+ padding: 0.05rem 0.375rem;
+ background: var(--bg-hover);
+ border: 0;
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-family: var(--mono);
+ font-size: 0.8125rem;
+}
+
+.web .images {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ margin: 0.5rem 0 0;
+ overflow: visible;
+ font-size: 0;
+}
+
+.web .images .image {
+ width: 5.5rem;
+ height: 5.5rem;
+ padding: 0;
+ line-height: 0;
+ border-radius: var(--radius-lg);
+ background: var(--bg-hover);
+ overflow: hidden;
+ transition: opacity var(--fast);
+}
+
+.web .images .image:hover {
+ opacity: 0.85;
+}
+
+.web .images .image img {
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ max-height: none;
+ object-fit: cover;
+ vertical-align: top;
+}
+
+.web .images .duration {
+ border: 0;
+ right: 0.375rem;
+ bottom: 0.375rem;
+}
+
+#images {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
+ gap: 1rem 0.75rem;
+ margin: 0;
+ padding: 0.5rem var(--gutter) 1.5rem;
+ overflow: visible;
+}
+
+#images .infobox {
+ grid-column: 1 / -1;
+ width: auto;
+ max-width: var(--main-width);
+ margin-bottom: 0.5rem;
+}
+
+#images .image-wrapper {
+ float: none;
+ width: auto;
+ line-height: 1.4;
+}
+
+#images .image {
+ width: auto;
+ max-width: none;
+ margin: 0;
+ padding: 0;
+ font-size: 0.8125rem;
+}
+
+#images .thumb {
+ height: 11.25rem;
+ margin-bottom: 0.5rem;
+ border-radius: var(--radius-lg);
+ background: var(--bg-hover);
+ overflow: hidden;
+ transition: opacity var(--fast);
+}
+
+#images .image:hover .thumb {
+ opacity: 0.85;
+}
+
+#images img {
+ object-fit: cover;
+}
+
+#images .duration {
+ border: 0;
+}
+
+#images .image .title,
+#images .image .i-title-wrapper {
+ margin-bottom: 0.125rem;
+ color: var(--text-primary);
+ font-size: 0.8125rem;
+ font-weight: 400;
+ text-overflow: ellipsis;
+}
+
+#images .image .description {
+ height: auto;
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.nextpage.img {
+ width: auto;
+ margin: 0 var(--gutter) 3rem;
+}
+
+#popup-bg {
+ background: #000;
+ opacity: 0.92;
+}
+
+#popup-image {
+ background: transparent;
+ border: 0;
+}
+
+#popup-status {
+ height: 2.75rem;
+ background: var(--bg-light);
+ border: 0;
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
+}
+
+#popup-num,
+#popup-title {
+ padding: 0 0.875rem;
+ line-height: 2.75rem;
+ color: var(--text-primary);
+ font-size: 0.8125rem;
+}
+
+#popup-num {
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+#popup-status select {
+ all: unset;
+ display: block;
+ width: 15rem;
+ height: 2rem;
+ margin-right: 0.75rem;
+ padding: 0 1.75rem 0 0.75rem;
+ background-color: var(--bg-hover);
+ background-image: var(--icon-chevron);
+ background-repeat: no-repeat;
+ background-position: right 0.625rem center;
+ background-size: 0.875rem;
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.8125rem;
+ line-height: 2rem;
+ cursor: pointer;
+}
+
+.captcha-wrapper {
+ border: 0;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.captcha-wrapper img {
+ background: var(--bg-light);
+}
+
+.captcha-wrapper input:checked + label::after {
+ border-right-color: var(--white);
+ border-bottom-color: var(--white);
+}
+
+.web .settings {
+ margin-top: 1rem;
+ padding: 1rem 1rem 0.25rem;
+}
+
+.web .settings h2 {
+ font-size: 1.125rem;
+ margin-bottom: 1rem;
+}
+
+.web .setting {
+ margin-bottom: 1.25rem;
+}
+
+.web .setting .title {
+ margin-bottom: 0.375rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ line-height: 1.5;
+}
+
+.web .setting .title i {
+ font-style: normal;
+ color: var(--text-secondary);
+}
+
+.web .settings select {
+ all: unset;
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 22rem;
+ height: 2.25rem;
+ padding: 0 2rem 0 0.75rem;
+ background-color: var(--bg);
+ background-image: var(--icon-chevron);
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 0.875rem;
+ border: 0;
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-family: var(--font);
+ font-size: 0.875rem;
+ line-height: 2.25rem;
+ cursor: pointer;
+}
+
+.web .settings-submit {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin: 1.25rem 0 0;
+}
+
+.web .settings-submit input {
+ all: unset;
+ order: 1;
+ height: 2.25rem;
+ padding: 0 1.25rem;
+ background: var(--primary);
+ border-radius: var(--radius-sm);
+ box-shadow: var(--shadow);
+ color: var(--white);
+ font-family: var(--font);
+ font-size: 0.875rem;
+ font-weight: 500;
+ line-height: 2.25rem;
+ cursor: pointer;
+ transition: background var(--fast);
+}
+
+.web .settings-submit input:hover {
+ background: var(--primary-hover);
+}
+
+.web .settings-submit a {
+ order: 2;
+ margin-right: 0;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+body.about {
+ margin: 0;
+ padding: 2rem var(--gutter) 4rem;
+ max-width: calc(var(--main-width) + var(--gutter) * 2);
+}
+
+.about a {
+ color: var(--text-link);
+}
+
+.about h1,
+.about h2 {
+ margin-top: 1.75rem;
+}
+
+.about table {
+ margin-bottom: 1rem;
+}
+
+body.instances {
+ margin: 0;
+ padding: 2rem var(--gutter) 4rem;
+}
+
+.instances a {
+ color: var(--text-link);
+}
+
+.instances table {
+ margin-top: 1rem;
+ background: var(--bg-light);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+}
+
+.instances thead {
+ outline: 0;
+ background: var(--bg-hover);
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+.instances tbody {
+ outline: 0;
+ top: 0;
+}
+
+.instances th,
+.instances td {
+ padding: 0.5rem 0.875rem;
+}
+
+.instances th:hover {
+ background: var(--border-light);
+}
+
+.instances tbody tr:nth-child(even) {
+ background: var(--bg-hover);
+}
+
+.instances tbody tr:hover {
+ background: var(--border-light);
+}
+
+.instances .arrow {
+ border-top-color: var(--text-secondary);
+}
+
+.instances .arrow.up {
+ border-top-color: transparent;
+ border-bottom-color: var(--text-secondary);
+}
+
+.instances .popup {
+ background: var(--bg-light);
+ border: 0;
+ border-radius: var(--radius-lg);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+ padding: 1rem 1.25rem;
+}
+
+.instances .go-back {
+ margin-top: 1rem;
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+}
+
+@media only screen and (max-width: 1550px) {
+ .web .right-right,
+ .web .right-left {
+ padding: 0;
+ }
+
+ .web .left,
+ .searchbox,
+ #images .infobox {
+ width: auto;
+ }
+
+ .web .right-wrapper {
+ width: var(--sidebar-width);
+ }
+}
+
+@media only screen and (max-width: 1000px) {
+ #overflow.web {
+ flex-direction: column;
+ gap: 0;
+ }
+
+ .web .left,
+ .web .right-wrapper {
+ width: 100%;
+ max-width: none;
+ }
+
+ .web .right-wrapper {
+ order: 0;
+ margin-bottom: 1.5rem;
+ }
+
+ .web.has-answer .left::before {
+ display: none;
+ }
+
+ body:not(.instances) table td {
+ display: table-cell;
+ width: auto;
+ }
+
+ .related td {
+ display: block;
+ width: 100%;
+ }
+
+ .web .info-table tr td:first-child,
+ .web .wiki-head tr td:first-child {
+ text-decoration: none;
+ }
+}
+
+@media only screen and (max-width: 767px) {
+ .navigation {
+ position: static;
+ justify-content: flex-end;
+ padding: 1rem var(--gutter) 0;
+ }
+
+ body > form {
+ grid-template-columns: minmax(0, 1fr);
+ padding-top: 0.5rem;
+ }
+
+ body > form .searchbox {
+ grid-column: 1;
+ grid-row: 1;
+ max-width: none;
+ }
+
+ body > form .tabs {
+ grid-row: 2;
+ }
+
+ body > form .filters {
+ display: flex;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ padding-bottom: 0.5rem;
+ scrollbar-width: thin;
+ }
+
+ .web .answer {
+ max-height: 16rem;
+ }
+
+ .thumb-wrap {
+ width: 5rem;
+ height: 3.75rem;
+ }
+}